Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Do not notify failures on collisions
  Fixed erroneous increment of the rescheduled metric
  Ensure failures are only incremented in RunnableWithMetrics
  Record replication metrics for retrying tasks
  Report deletions as succeeded in the fetch-ref-replicated events
  NOOP fetches are to be considered succeded
  Properly propagate failure when ref deletion fails
  Accept remotes without `fetch` option on primary
  Pull-replication plugin should warn about inconsistent timeouts
  Remove events from beforeStartupEventsQueue after fire()
  Fix ordering of parameters for apply objects log
  Respect `remote.mirror` option in DeleteRefCommand
  Remove events from beforeStartupEventsQueue in-memory queue
  Reverse the order of storing commit, tree and blobs
  Do not honor gerrit.replicateOnStartup on primary side
  Do not record fetch replication metrics when all refs are excluded from fetch task
  Don't require remote.<name>.url on primary side
  Use GJF 1.7 for code-style verification

Change-Id: I0cb49e9761e5783a024899db00078ca75338e77a
diff --git a/example-setup/broker/Dockerfile b/example-setup/broker/Dockerfile
index 08eaba9..69261c7 100644
--- a/example-setup/broker/Dockerfile
+++ b/example-setup/broker/Dockerfile
@@ -1,8 +1,10 @@
-FROM gerritcodereview/gerrit:3.5.5-almalinux8
+FROM gerritcodereview/gerrit:3.6.3-almalinux8
 
 USER root
 
-RUN yum install -y gettext
+RUN yes | rpm --import https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux && \
+    yes | dnf upgrade almalinux-release && \
+    yum install -y gettext
 
 RUN git config -f /var/gerrit/etc/secure.config --add auth.bearerToken "theSecretBearerToken"
 
@@ -12,7 +14,7 @@
 # hence rename it with a 'z-' prefix because the Gerrit plugin loader starts the
 # plugins in filename alphabetical order.
 COPY --chown=gerrit:gerrit events-kafka.jar /var/gerrit/plugins/z-events-kafka.jar
-COPY --chown=gerrit:gerrit libevents-broker.jar /var/gerrit/lib/libevents-broker.jar
+COPY --chown=gerrit:gerrit events-broker.jar /var/gerrit/lib/events-broker.jar
 
 COPY --chown=gerrit:gerrit entrypoint.sh /tmp/
 COPY --chown=gerrit:gerrit configs/replication.config.template /var/gerrit/etc/
diff --git a/example-setup/http/Dockerfile b/example-setup/http/Dockerfile
index e9f8239..77fed72 100644
--- a/example-setup/http/Dockerfile
+++ b/example-setup/http/Dockerfile
@@ -1,4 +1,4 @@
-FROM gerritcodereview/gerrit:3.5.5-almalinux8
+FROM gerritcodereview/gerrit:3.6.3-almalinux8
 
 USER root
 
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 e83590a..df39d5a 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
@@ -345,8 +345,9 @@
 
       if (fetchRefSpecs.isEmpty()) {
         repLog.info(
-            "[{}] Replication from {} finished but no refs were replicated, {}ms delay, {} retries",
+            "[{}] {} replication from {} finished but no refs were replicated, {}ms delay, {} retries",
             taskIdHex,
+            replicationType,
             uri,
             delay,
             retryCount);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
index 72a6b74..005d383 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.replication.pull;
 
 import static com.googlesource.gerrit.plugins.replication.StartReplicationCapability.START_REPLICATION;
+import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
 
 import com.google.common.eventbus.EventBus;
 import com.google.gerrit.extensions.annotations.Exports;
@@ -44,8 +45,8 @@
 import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
 import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
 import com.googlesource.gerrit.plugins.replication.StartReplicationCapability;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
-import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiModule;
 import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
@@ -82,13 +83,16 @@
         .annotatedWith(Names.named(ReplicationQueueMetrics.REPLICATION_QUEUE_METRICS))
         .toInstance(pluginMetricMaker);
 
+    bind(CapabilityDefinition.class)
+        .annotatedWith(Exports.named(CALL_FETCH_ACTION))
+        .to(FetchApiCapability.class);
+
     install(new PullReplicationGroupModule());
     bind(BearerTokenProvider.class).in(Scopes.SINGLETON);
     bind(RevisionReader.class).in(Scopes.SINGLETON);
     bind(ApplyObject.class);
     install(new FactoryModuleBuilder().build(FetchJob.Factory.class));
     install(new ApplyObjectCacheModule());
-    install(new PullReplicationApiModule());
 
     install(new FetchRefReplicatedEventModule());
 
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 b347bc7..ef1c2fd 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
@@ -181,12 +181,7 @@
             event.getRefName(),
             event.refUpdate.get().oldRev,
             event.refUpdate.get().newRev);
-        fire(
-            event.refUpdate.get().project,
-            ObjectId.fromString(event.refUpdate.get().newRev),
-            event.getRefName(),
-            event.eventCreatedOn,
-            ZEROS_OBJECTID.equals(event.refUpdate.get().newRev));
+        fire(ReferenceUpdatedEvent.from(event));
       }
     }
   }
@@ -215,42 +210,45 @@
     return !refsFilter.match(refName);
   }
 
-  private void fire(
-      String projectName,
-      ObjectId objectId,
-      String refName,
-      long eventCreatedOn,
-      boolean isDelete) {
+  private void fire(ReferenceUpdatedEvent event) {
     ReplicationState state = new ReplicationState(new GitUpdateProcessing(dispatcher.get()));
-    fire(Project.nameKey(projectName), objectId, refName, eventCreatedOn, isDelete, state);
+    fire(event, state);
     state.markAllFetchTasksScheduled();
   }
 
-  private void fire(
-      NameKey project,
-      ObjectId objectId,
-      String refName,
-      long eventCreatedOn,
-      boolean isDelete,
-      ReplicationState state) {
+  private void fire(ReferenceUpdatedEvent event, ReplicationState state) {
     if (!running) {
       stateLog.warn(
-          "Replication plugin did not finish startup before event, event replication is postponed",
+          String.format(
+              "Replication plugin did not finish startup before event, event replication is postponed"
+                  + " for event %s",
+              event),
           state);
-      beforeStartupEventsQueue.add(
-          ReferenceUpdatedEvent.create(project.get(), refName, objectId, eventCreatedOn, isDelete));
+      beforeStartupEventsQueue.add(event);
 
       queueMetrics.incrementQueuedBeforStartup();
       return;
     }
     ForkJoinPool fetchCallsPool = null;
     try {
-      fetchCallsPool = new ForkJoinPool(sources.get().getAll().size());
+      List<Source> allSources = sources.get().getAll();
+      int numSources = allSources.size();
+      if (numSources == 0) {
+        repLog.debug("No replication sources configured -> skipping fetch");
+        return;
+      }
+      fetchCallsPool = new ForkJoinPool(numSources);
 
       final Consumer<Source> callFunction =
-          callFunction(project, objectId, refName, eventCreatedOn, isDelete, state);
+          callFunction(
+              Project.nameKey(event.projectName()),
+              event.objectId(),
+              event.refName(),
+              event.eventCreatedOn(),
+              event.isDelete(),
+              state);
       fetchCallsPool
-          .submit(() -> sources.get().getAll().parallelStream().forEach(callFunction))
+          .submit(() -> allSources.parallelStream().forEach(callFunction))
           .get(fetchCallsTimeout, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | ExecutionException | TimeoutException e) {
       stateLog.error(
@@ -523,7 +521,20 @@
   private HttpResult initProject(
       Project.NameKey project, URIish uri, FetchApiClient fetchClient, HttpResult result)
       throws IOException, ClientProtocolException {
-    HttpResult initProjectResult = fetchClient.initProject(project, uri);
+    RevisionData refsMetaConfigRevisionData =
+        revReaderProvider
+            .get()
+            .read(project, null, RefNames.REFS_CONFIG, 0)
+            .orElseThrow(
+                () ->
+                    new IllegalStateException(
+                        String.format(
+                            "Project %s does not have %s", project, RefNames.REFS_CONFIG)));
+
+    List<RevisionData> refsMetaConfigDataList =
+        fetchWholeMetaHistory(project, RefNames.REFS_CONFIG, refsMetaConfigRevisionData);
+    HttpResult initProjectResult =
+        fetchClient.initProject(project, uri, System.currentTimeMillis(), refsMetaConfigDataList);
     if (initProjectResult.isSuccessful()) {
       result = fetchClient.callFetch(project, FetchOne.ALL_REFS, uri);
     } else {
@@ -540,12 +551,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(),
-            event.eventCreatedOn(),
-            event.isDelete());
+        fire(event);
         eventsReplayed.add(eventKey);
       }
       beforeStartupEventsQueue.remove(event);
@@ -580,6 +586,15 @@
           projectName, refName, objectId, eventCreatedOn, isDelete);
     }
 
+    static ReferenceUpdatedEvent from(RefUpdatedEvent event) {
+      return ReferenceUpdatedEvent.create(
+          event.refUpdate.get().project,
+          event.getRefName(),
+          ObjectId.fromString(event.refUpdate.get().newRev),
+          event.eventCreatedOn,
+          ZEROS_OBJECTID.equals(event.refUpdate.get().newRev));
+    }
+
     public abstract String projectName();
 
     public abstract String refName();
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 b792c2a..01d32fb 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
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingLatestPatchSetException;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
@@ -35,6 +36,7 @@
 import java.util.Objects;
 import javax.servlet.http.HttpServletResponse;
 
+@Singleton
 public class ApplyObjectAction implements RestModifyView<ProjectResource, RevisionInput> {
 
   private final ApplyObjectCommand applyObjectCommand;
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
index 04eca3a..5192351 100644
--- 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
@@ -36,7 +36,6 @@
 import com.googlesource.gerrit.plugins.replication.pull.Source;
 import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.DeleteRefException;
-import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
 import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
 import java.io.IOException;
 import java.util.Optional;
@@ -50,7 +49,6 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PullReplicationStateLogger fetchStateLog;
-  private final ApplyObject applyObject;
   private final DynamicItem<EventDispatcher> eventDispatcher;
   private final ProjectCache projectCache;
   private final SourcesCollection sourcesCollection;
@@ -62,13 +60,11 @@
       PullReplicationStateLogger fetchStateLog,
       ProjectCache projectCache,
       SourcesCollection sourcesCollection,
-      ApplyObject applyObject,
       PermissionBackend permissionBackend,
       DynamicItem<EventDispatcher> eventDispatcher,
       LocalGitRepositoryManagerProvider gitManagerProvider) {
     this.fetchStateLog = fetchStateLog;
     this.projectCache = projectCache;
-    this.applyObject = applyObject;
     this.eventDispatcher = eventDispatcher;
     this.sourcesCollection = sourcesCollection;
     this.permissionBackend = permissionBackend;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
index 04797bd..9e69f8d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException;
@@ -38,6 +39,7 @@
 import java.util.concurrent.TimeoutException;
 import org.eclipse.jgit.errors.TransportException;
 
+@Singleton
 public class FetchAction implements RestModifyView<ProjectResource, Input> {
   private final FetchCommand command;
   private final WorkQueue workQueue;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
index 73a4ac5..27afcfd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 
 public class FetchApiCapability extends CapabilityDefinition {
-  static final String CALL_FETCH_ACTION = "callFetchAction";
+  public static final String CALL_FETCH_ACTION = "callFetchAction";
 
   @Override
   public String getDescription() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
index b8249ae..5323425 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
@@ -97,7 +97,12 @@
       if (fetchType == ReplicationType.ASYNC) {
         state.markAllFetchTasksScheduled();
         Future<?> future = source.get().schedule(name, refName, state, apiRequestMetrics);
-        future.get(source.get().getTimeout(), TimeUnit.SECONDS);
+        int timeout = source.get().getTimeout();
+        if (timeout == 0) {
+          future.get();
+        } else {
+          future.get(timeout, TimeUnit.SECONDS);
+        }
       } else {
         Optional<FetchOne> maybeFetch =
             source
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
index 77d0e0b..7ca8805 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
@@ -26,8 +26,10 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
 
+@Singleton
 public class FetchPreconditions {
   private final String pluginName;
   private final PermissionBackend permissionBackend;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
index 0f3e1e8..95082b8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.httpd.AllRequestFilter;
-import com.google.gerrit.server.config.GerritIsReplica;
 import com.google.inject.Inject;
 import com.google.inject.Scopes;
 import com.google.inject.name.Names;
@@ -24,12 +23,10 @@
 import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
 
 public class HttpModule extends ServletModule {
-  private boolean isReplica;
   private final BearerTokenProvider bearerTokenProvider;
 
   @Inject
-  public HttpModule(@GerritIsReplica Boolean isReplica, BearerTokenProvider bearerTokenProvider) {
-    this.isReplica = isReplica;
+  public HttpModule(BearerTokenProvider bearerTokenProvider) {
     this.bearerTokenProvider = bearerTokenProvider;
   }
 
@@ -49,12 +46,8 @@
                   .in(Scopes.SINGLETON);
             });
 
-    if (isReplica) {
-      DynamicSet.bind(binder(), AllRequestFilter.class)
-          .to(PullReplicationFilter.class)
-          .in(Scopes.SINGLETON);
-    } else {
-      serveRegex("/init-project/.*$").with(ProjectInitializationAction.class);
-    }
+    DynamicSet.bind(binder(), AllRequestFilter.class)
+        .to(PullReplicationFilter.class)
+        .in(Scopes.SINGLETON);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
index 2e1c5d4..adb333c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
@@ -27,11 +27,13 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.LocalFS;
 import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
 import java.util.Optional;
 import org.eclipse.jgit.transport.URIish;
 
+@Singleton
 class ProjectDeletionAction
     implements RestModifyView<ProjectResource, ProjectDeletionAction.DeleteInput> {
   private static final PluginPermission DELETE_PROJECT =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
index 8711379..4ed3160 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
@@ -14,25 +14,40 @@
 
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
+import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
 import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader;
 import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.setResponse;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 
+import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.net.MediaType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.index.project.ProjectIndexer;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.LocalFS;
 import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingLatestPatchSetException;
+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.api.util.PayloadSerDes;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
 import java.util.Optional;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
@@ -51,17 +66,23 @@
   private final Provider<CurrentUser> userProvider;
   private final PermissionBackend permissionBackend;
   private final ProjectIndexer projectIndexer;
+  private final ApplyObjectCommand applyObjectCommand;
+  private final ProjectCache projectCache;
 
   @Inject
   ProjectInitializationAction(
       GerritConfigOps gerritConfigOps,
       Provider<CurrentUser> userProvider,
       PermissionBackend permissionBackend,
-      ProjectIndexer projectIndexer) {
+      ProjectIndexer projectIndexer,
+      ApplyObjectCommand applyObjectCommand,
+      ProjectCache projectCache) {
     this.gerritConfigOps = gerritConfigOps;
     this.userProvider = userProvider;
     this.permissionBackend = permissionBackend;
     this.projectIndexer = projectIndexer;
+    this.applyObjectCommand = applyObjectCommand;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -73,47 +94,151 @@
       return;
     }
 
-    String path = httpServletRequest.getRequestURI();
-    String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1));
+    String gitRepositoryName = getGitRepositoryName(httpServletRequest);
     try {
-      if (initProject(projectName)) {
+      boolean initProjectStatus;
+      String contentType = httpServletRequest.getContentType();
+      if (checkContentType(contentType, MediaType.JSON_UTF_8)) {
+        // init project request includes project configuration in JSON format.
+        initProjectStatus = initProjectWithConfiguration(httpServletRequest, gitRepositoryName);
+      } else if (checkContentType(contentType, MediaType.PLAIN_TEXT_UTF_8)) {
+        // init project request does not include project configuration.
+        initProjectStatus = initProject(gitRepositoryName);
+      } else {
+        setResponse(
+            httpServletResponse,
+            SC_BAD_REQUEST,
+            String.format(
+                "Invalid Content Type. Only %s or %s is supported.",
+                MediaType.JSON_UTF_8.toString(), MediaType.PLAIN_TEXT_UTF_8.toString()));
+        return;
+      }
+
+      if (initProjectStatus) {
         setResponse(
             httpServletResponse,
             HttpServletResponse.SC_CREATED,
-            "Project " + projectName + " initialized");
+            "Project " + gitRepositoryName + " initialized");
         return;
       }
-    } catch (AuthException | PermissionBackendException e) {
+
       setResponse(
           httpServletResponse,
-          HttpServletResponse.SC_FORBIDDEN,
-          "User not authorized to create project " + projectName);
-      return;
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          "Cannot initialize project " + gitRepositoryName);
+    } catch (BadRequestException | IllegalArgumentException e) {
+      logExceptionAndUpdateResponse(httpServletResponse, e, SC_BAD_REQUEST, gitRepositoryName);
+    } catch (RefUpdateException | MissingParentObjectException e) {
+      logExceptionAndUpdateResponse(httpServletResponse, e, SC_CONFLICT, gitRepositoryName);
+    } catch (AuthException | PermissionBackendException e) {
+      logExceptionAndUpdateResponse(httpServletResponse, e, SC_FORBIDDEN, gitRepositoryName);
     }
-
-    setResponse(
-        httpServletResponse,
-        HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
-        "Cannot initialize project " + projectName);
   }
 
-  public boolean initProject(String projectName) throws AuthException, PermissionBackendException {
+  public boolean initProject(String gitRepositoryName)
+      throws AuthException, PermissionBackendException {
+    if (initProject(gitRepositoryName, true)) {
+      repLog.info("Init project API from {}", gitRepositoryName);
+      return true;
+    }
+    return false;
+  }
+
+  private boolean initProjectWithConfiguration(
+      HttpServletRequest httpServletRequest, String gitRepositoryName)
+      throws AuthException, PermissionBackendException, IOException, BadRequestException,
+          MissingParentObjectException, RefUpdateException {
+
+    RevisionsInput input = PayloadSerDes.parseRevisionsInput(httpServletRequest);
+    validateInput(input);
+    if (!initProject(gitRepositoryName, false)) {
+      return false;
+    }
+
+    String projectName = gitRepositoryName.replace(".git", "");
+    try {
+      applyObjectCommand.applyObjects(
+          Project.nameKey(projectName),
+          input.getRefName(),
+          input.getRevisionsData(),
+          input.getLabel(),
+          input.getEventCreatedOn());
+    } catch (MissingLatestPatchSetException e) {
+      repLog.error(
+          "Init project API FAILED from {} for {} - configuration data cannot contain change meta refs: {}:{}",
+          input.getLabel(),
+          projectName,
+          input.getRefName(),
+          Arrays.toString(input.getRevisionsData()),
+          e);
+      throw new BadRequestException("Configuration data cannot contain change meta refs", e);
+    }
+    projectCache.onCreateProject(Project.nameKey(projectName));
+    repLog.info(
+        "Init project API from {} for {}:{} - {}",
+        input.getLabel(),
+        projectName,
+        input.getRefName(),
+        Arrays.toString(input.getRevisionsData()));
+    return true;
+  }
+
+  private boolean initProject(String gitRepositoryName, boolean needsProjectReindexing)
+      throws AuthException, PermissionBackendException {
     // When triggered internally(for example by consuming stream events) user is not provided
     // and internal user is returned. Project creation should be always allowed for internal user.
     if (!userProvider.get().isInternalUser()) {
       permissionBackend.user(userProvider.get()).check(GlobalPermission.CREATE_PROJECT);
     }
-    Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(projectName);
+    Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(gitRepositoryName);
     if (!maybeUri.isPresent()) {
-      logger.atSevere().log("Cannot initialize project '%s'", projectName);
+      logger.atSevere().log("Cannot initialize project '%s'", gitRepositoryName);
       return false;
     }
     LocalFS localFS = new LocalFS(maybeUri.get());
-    Project.NameKey projectNameKey = Project.NameKey.parse(projectName);
+    Project.NameKey projectNameKey = Project.NameKey.parse(gitRepositoryName);
     if (localFS.createProject(projectNameKey, RefNames.HEAD)) {
-      projectIndexer.index(projectNameKey);
+      if (needsProjectReindexing) {
+        projectIndexer.index(projectNameKey);
+      }
       return true;
     }
     return false;
   }
+
+  private void validateInput(RevisionsInput input) {
+
+    if (Strings.isNullOrEmpty(input.getLabel())) {
+      throw new IllegalArgumentException("Source label cannot be null or empty");
+    }
+
+    if (!Objects.equals(input.getRefName(), RefNames.REFS_CONFIG)) {
+      throw new IllegalArgumentException(
+          String.format("Ref-update refname should be %s", RefNames.REFS_CONFIG));
+    }
+    input.validate();
+  }
+
+  private String getGitRepositoryName(HttpServletRequest httpServletRequest) {
+    String path = httpServletRequest.getRequestURI();
+    return Url.decode(path.substring(path.lastIndexOf('/') + 1));
+  }
+
+  private void logExceptionAndUpdateResponse(
+      HttpServletResponse httpServletResponse,
+      Exception e,
+      int statusCode,
+      String gitRepositoryName)
+      throws IOException {
+    repLog.error("Init Project API FAILED for {}", gitRepositoryName, e);
+    setResponse(httpServletResponse, statusCode, e.getMessage());
+  }
+
+  private boolean checkContentType(String contentType, MediaType mediaType) {
+    try {
+      return MediaType.parse(contentType).is(mediaType);
+    } catch (Exception e) {
+      return false;
+    }
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
deleted file mode 100644
index d1d28a6..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.googlesource.gerrit.plugins.replication.pull.api;
-
-import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
-import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
-
-import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.extensions.config.CapabilityDefinition;
-import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.inject.Scopes;
-
-public class PullReplicationApiModule extends RestApiModule {
-  @Override
-  protected void configure() {
-    bind(FetchAction.class).in(Scopes.SINGLETON);
-    bind(ApplyObjectAction.class).in(Scopes.SINGLETON);
-    bind(ProjectDeletionAction.class).in(Scopes.SINGLETON);
-    bind(UpdateHeadAction.class).in(Scopes.SINGLETON);
-    post(PROJECT_KIND, "fetch").to(FetchAction.class);
-    post(PROJECT_KIND, "apply-object").to(ApplyObjectAction.class);
-    post(PROJECT_KIND, "apply-objects").to(ApplyObjectsAction.class);
-    delete(PROJECT_KIND, "delete-project").to(ProjectDeletionAction.class);
-    put(PROJECT_KIND, "HEAD").to(UpdateHeadAction.class);
-
-    bind(FetchPreconditions.class).in(Scopes.SINGLETON);
-    bind(CapabilityDefinition.class)
-        .annotatedWith(Exports.named(CALL_FETCH_ACTION))
-        .to(FetchApiCapability.class);
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
index e54d408..d0e5c10 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -16,16 +16,14 @@
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY;
 import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader;
-import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.setResponse;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
-import static javax.servlet.http.HttpServletResponse.SC_CREATED;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.projects.HeadInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -35,29 +33,25 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
-import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.ProjectsCollection;
-import com.google.gson.Gson;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gson.JsonParseException;
-import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.MalformedJsonException;
 import com.google.inject.Inject;
-import com.google.inject.TypeLiteral;
+import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
-import com.googlesource.gerrit.plugins.replication.pull.api.exception.InitProjectException;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
-import java.io.BufferedReader;
-import java.io.EOFException;
+import com.googlesource.gerrit.plugins.replication.pull.api.util.PayloadSerDes;
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Optional;
@@ -83,9 +77,9 @@
   private ProjectInitializationAction projectInitializationAction;
   private UpdateHeadAction updateHEADAction;
   private ProjectDeletionAction projectDeletionAction;
-  private ProjectsCollection projectsCollection;
-  private Gson gson;
+  private ProjectCache projectCache;
   private String pluginName;
+  private final Provider<CurrentUser> currentUserProvider;
 
   @Inject
   public PullReplicationFilter(
@@ -95,17 +89,18 @@
       ProjectInitializationAction projectInitializationAction,
       UpdateHeadAction updateHEADAction,
       ProjectDeletionAction projectDeletionAction,
-      ProjectsCollection projectsCollection,
-      @PluginName String pluginName) {
+      ProjectCache projectCache,
+      @PluginName String pluginName,
+      Provider<CurrentUser> currentUserProvider) {
     this.fetchAction = fetchAction;
     this.applyObjectAction = applyObjectAction;
     this.applyObjectsAction = applyObjectsAction;
     this.projectInitializationAction = projectInitializationAction;
     this.updateHEADAction = updateHEADAction;
     this.projectDeletionAction = projectDeletionAction;
-    this.projectsCollection = projectsCollection;
+    this.projectCache = projectCache;
     this.pluginName = pluginName;
-    this.gson = OutputFormat.JSON.newGsonBuilder().create();
+    this.currentUserProvider = currentUserProvider;
   }
 
   @Override
@@ -120,20 +115,26 @@
     HttpServletRequest httpRequest = (HttpServletRequest) request;
     try {
       if (isFetchAction(httpRequest)) {
-        writeResponse(httpResponse, doFetch(httpRequest));
+        failIfcurrentUserIsAnonymous();
+        PayloadSerDes.writeResponse(httpResponse, doFetch(httpRequest));
       } else if (isApplyObjectAction(httpRequest)) {
-        writeResponse(httpResponse, doApplyObject(httpRequest));
+        failIfcurrentUserIsAnonymous();
+        PayloadSerDes.writeResponse(httpResponse, doApplyObject(httpRequest));
       } else if (isApplyObjectsAction(httpRequest)) {
-        writeResponse(httpResponse, doApplyObjects(httpRequest));
+        failIfcurrentUserIsAnonymous();
+        PayloadSerDes.writeResponse(httpResponse, doApplyObjects(httpRequest));
       } else if (isInitProjectAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
         if (!checkAcceptHeader(httpRequest, httpResponse)) {
           return;
         }
         doInitProject(httpRequest, httpResponse);
       } else if (isUpdateHEADAction(httpRequest)) {
-        writeResponse(httpResponse, doUpdateHEAD(httpRequest));
+        failIfcurrentUserIsAnonymous();
+        PayloadSerDes.writeResponse(httpResponse, doUpdateHEAD(httpRequest));
       } else if (isDeleteProjectAction(httpRequest)) {
-        writeResponse(httpResponse, doDeleteProject(httpRequest));
+        failIfcurrentUserIsAnonymous();
+        PayloadSerDes.writeResponse(httpResponse, doDeleteProject(httpRequest));
       } else {
         chain.doFilter(request, response);
       }
@@ -156,7 +157,7 @@
     } catch (ResourceConflictException e) {
       RestApiServlet.replyError(
           httpRequest, httpResponse, SC_CONFLICT, e.getMessage(), e.caching(), e);
-    } catch (InitProjectException | ResourceNotFoundException e) {
+    } catch (ResourceNotFoundException e) {
       RestApiServlet.replyError(
           httpRequest, httpResponse, SC_INTERNAL_SERVER_ERROR, e.getMessage(), e.caching(), e);
     } catch (NoSuchElementException e) {
@@ -173,108 +174,67 @@
     }
   }
 
-  private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
-      throws RestApiException, IOException, PermissionBackendException {
-
-    IdString id = getInitProjectName(httpRequest).get();
-    String projectName = id.get();
-    if (projectInitializationAction.initProject(projectName)) {
-      setResponse(
-          httpResponse, HttpServletResponse.SC_CREATED, "Project " + projectName + " initialized");
-      return;
+  private void failIfcurrentUserIsAnonymous() throws UnauthorizedAuthException {
+    CurrentUser currentUser = currentUserProvider.get();
+    if (currentUser instanceof AnonymousUser) {
+      throw new UnauthorizedAuthException();
     }
-    throw new InitProjectException(projectName);
+  }
+
+  private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
+      throws IOException, ServletException {
+    projectInitializationAction.service(httpRequest, httpResponse);
   }
 
   @SuppressWarnings("unchecked")
   private Response<String> doApplyObject(HttpServletRequest httpRequest)
       throws RestApiException, IOException, PermissionBackendException {
-    RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class));
+    RevisionInput input = PayloadSerDes.parseRevisionInput(httpRequest);
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<String>) applyObjectAction.apply(projectResource, input);
+    return (Response<String>) applyObjectAction.apply(parseProjectResource(id), input);
   }
 
   @SuppressWarnings("unchecked")
   private Response<String> doApplyObjects(HttpServletRequest httpRequest)
       throws RestApiException, IOException, PermissionBackendException {
-    RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class));
+    RevisionsInput input = PayloadSerDes.parseRevisionsInput(httpRequest);
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<String>) applyObjectsAction.apply(projectResource, input);
+    return (Response<String>) applyObjectsAction.apply(parseProjectResource(id), input);
   }
 
   @SuppressWarnings("unchecked")
   private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception {
-    HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class));
+    HeadInput input = PayloadSerDes.parseHeadInput(httpRequest);
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<String>) updateHEADAction.apply(projectResource, input);
+    return (Response<String>) updateHEADAction.apply(parseProjectResource(id), input);
   }
 
   @SuppressWarnings("unchecked")
   private Response<String> doDeleteProject(HttpServletRequest httpRequest) throws Exception {
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
     return (Response<String>)
-        projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput());
+        projectDeletionAction.apply(
+            parseProjectResource(id), new ProjectDeletionAction.DeleteInput());
   }
 
   @SuppressWarnings("unchecked")
   private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest)
       throws IOException, RestApiException, PermissionBackendException {
-    Input input = readJson(httpRequest, TypeLiteral.get(Input.class));
+    Input input = PayloadSerDes.parseInput(httpRequest);
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input);
+    return (Response<Map<String, Object>>) fetchAction.apply(parseProjectResource(id), input);
   }
 
-  private <T> void writeResponse(HttpServletResponse httpResponse, Response<T> response)
-      throws IOException {
-    String responseJson = gson.toJson(response);
-    if (response.statusCode() == SC_OK || response.statusCode() == SC_CREATED) {
-
-      httpResponse.setContentType("application/json");
-      httpResponse.setStatus(response.statusCode());
-      PrintWriter writer = httpResponse.getWriter();
-      writer.print(new String(RestApiServlet.JSON_MAGIC));
-      writer.print(responseJson);
-    } else {
-      httpResponse.sendError(response.statusCode(), responseJson);
+  private ProjectResource parseProjectResource(IdString id) throws ResourceNotFoundException {
+    Optional<ProjectState> project = projectCache.get(Project.nameKey(id.get()));
+    if (project.isEmpty()) {
+      throw new ResourceNotFoundException(id);
     }
-  }
-
-  private <T> T readJson(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral)
-      throws IOException, BadRequestException {
-
-    try (BufferedReader br = httpRequest.getReader();
-        JsonReader json = new JsonReader(br)) {
-      try {
-        json.setLenient(true);
-
-        try {
-          json.peek();
-        } catch (EOFException e) {
-          throw new BadRequestException("Expected JSON object", e);
-        }
-
-        return gson.fromJson(json, typeLiteral.getType());
-      } finally {
-        try {
-          // Reader.close won't consume the rest of the input. Explicitly consume the request
-          // body.
-          br.skip(Long.MAX_VALUE);
-        } catch (Exception e) {
-          // ignore, e.g. trying to consume the rest of the input may fail if the request was
-          // cancelled
-          logger.atFine().withCause(e).log("Exception during the parsing of the request json");
-        }
-      }
-    }
+    return new ProjectResource(project.get(), currentUserProvider.get());
   }
 
   /**
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java
deleted file mode 100644
index 85a7729..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2021 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.exception;
-
-import com.google.gerrit.extensions.restapi.RestApiException;
-
-public class InitProjectException extends RestApiException {
-  private static final long serialVersionUID = 1L;
-
-  public InitProjectException(String projectName) {
-    super("Cannot create project " + projectName);
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java
new file mode 100644
index 0000000..414af37
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2023 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.util;
+
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.projects.HeadInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gson.Gson;
+import com.google.gson.stream.JsonReader;
+import com.google.inject.TypeLiteral;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class PayloadSerDes {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final Gson gson = OutputFormat.JSON.newGsonBuilder().create();
+
+  public static RevisionInput parseRevisionInput(HttpServletRequest httpRequest)
+      throws BadRequestException, IOException {
+    return parse(httpRequest, TypeLiteral.get(RevisionInput.class));
+  }
+
+  public static RevisionsInput parseRevisionsInput(HttpServletRequest httpRequest)
+      throws BadRequestException, IOException {
+    return parse(httpRequest, TypeLiteral.get(RevisionsInput.class));
+  }
+
+  public static HeadInput parseHeadInput(HttpServletRequest httpRequest)
+      throws BadRequestException, IOException {
+    return parse(httpRequest, TypeLiteral.get(HeadInput.class));
+  }
+
+  public static FetchAction.Input parseInput(HttpServletRequest httpRequest)
+      throws BadRequestException, IOException {
+    return parse(httpRequest, TypeLiteral.get(FetchAction.Input.class));
+  }
+
+  public static <T> void writeResponse(HttpServletResponse httpResponse, Response<T> response)
+      throws IOException {
+    String responseJson = gson.toJson(response);
+    if (response.statusCode() == SC_OK || response.statusCode() == SC_CREATED) {
+
+      httpResponse.setContentType("application/json");
+      httpResponse.setStatus(response.statusCode());
+      PrintWriter writer = httpResponse.getWriter();
+      writer.print(new String(RestApiServlet.JSON_MAGIC));
+      writer.print(responseJson);
+    } else {
+      httpResponse.sendError(response.statusCode(), responseJson);
+    }
+  }
+
+  private static <T> T parse(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral)
+      throws IOException, BadRequestException {
+
+    try (BufferedReader br = httpRequest.getReader();
+        JsonReader json = new JsonReader(br)) {
+      try {
+        json.setLenient(true);
+
+        try {
+          json.peek();
+        } catch (EOFException e) {
+          throw new BadRequestException("Expected JSON object", e);
+        }
+
+        return gson.fromJson(json, typeLiteral.getType());
+      } finally {
+        try {
+          // Reader.close won't consume the rest of the input. Explicitly consume the request
+          // body.
+          br.skip(Long.MAX_VALUE);
+        } catch (Exception e) {
+          // ignore, e.g. trying to consume the rest of the input may fail if the request was
+          // cancelled
+          logger.atFine().withCause(e).log("Exception during the parsing of the request json");
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
index 1991260..f7ed4cb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
@@ -40,7 +40,22 @@
     return callFetch(project, refName, targetUri, MILLISECONDS.toNanos(System.currentTimeMillis()));
   }
 
-  HttpResult initProject(Project.NameKey project, URIish uri) throws IOException;
+  /**
+   * Replicates the creation of a project, including the configuration stored in refs/meta/config.
+   *
+   * @param project The unique name of the project.
+   * @param uri The destination URI where the project and its configuration should be replicated to.
+   * @param eventCreatedOn The timestamp indicating when the init project event occurred.
+   * @param refsMetaConfigRevisionData A history of revisions for the refs/meta/config ref.
+   * @return An HTTP result object providing information about the replication process.
+   * @throws IOException If an I/O error occurs during the replication.
+   */
+  HttpResult initProject(
+      Project.NameKey project,
+      URIish uri,
+      long eventCreatedOn,
+      List<RevisionData> refsMetaConfigRevisionData)
+      throws IOException;
 
   HttpResult deleteProject(Project.NameKey project, URIish apiUri) throws IOException;
 
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 b606ba8..7607e4b 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.config.GerritInstanceId;
@@ -130,14 +131,29 @@
   }
 
   /* (non-Javadoc)
-   * @see com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient#initProject(com.google.gerrit.entities.Project.NameKey, org.eclipse.jgit.transport.URIish)
+   * @see com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient#initProject(com.google.gerrit.entities.Project.NameKey, org.eclipse.jgit.transport.URIish, long, java.util.List<com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData>)
    */
   @Override
-  public HttpResult initProject(Project.NameKey project, URIish uri) throws IOException {
+  public HttpResult initProject(
+      NameKey project,
+      URIish uri,
+      long eventCreatedOn,
+      List<RevisionData> refsMetaConfigRevisionData)
+      throws IOException {
     String url = formatInitProjectUrl(uri.toString(), project);
+
+    RevisionData[] inputData = new RevisionData[refsMetaConfigRevisionData.size()];
+    RevisionsInput input =
+        new RevisionsInput(
+            instanceId,
+            RefNames.REFS_CONFIG,
+            eventCreatedOn,
+            refsMetaConfigRevisionData.toArray(inputData));
+
     HttpPut put = new HttpPut(url);
+    put.setEntity(new StringEntity(GSON.toJson(input)));
     put.addHeader(new BasicHeader("Accept", MediaType.ANY_TEXT_TYPE.toString()));
-    put.addHeader(new BasicHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString()));
+    put.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString()));
     return executeRequest(put, bearerTokenProvider.get(), uri);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
index 8fac9ed..43ec9bf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
@@ -28,6 +28,8 @@
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -107,7 +109,9 @@
     if (credentialsProvider.supports(user, pass)
         && credentialsProvider.get(uri, user, pass)
         && uri.getScheme() != null
-        && !"ssh".equalsIgnoreCase(uri.getScheme())) {
+        && !"ssh".equalsIgnoreCase(uri.getScheme())
+        && StringUtils.isNotEmpty(user.getValue())
+        && ArrayUtils.isNotEmpty(pass.getValue())) {
       return uri.setUser(user.getValue()).setPass(String.valueOf(pass.getValue()));
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java
index 9a10898..434c0f0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java
@@ -35,7 +35,7 @@
 
   @Override
   public Void visit(AssistedInjectBinding<? extends FetchFactory> binding) {
-    TypeLiteral<CGitFetch> nativeGitFetchType = new TypeLiteral<CGitFetch>() {};
+    TypeLiteral<CGitFetch> nativeGitFetchType = new TypeLiteral<>() {};
     for (AssistedMethod method : binding.getAssistedMethods()) {
       if (method.getImplementationType().equals(nativeGitFetchType)) {
         String[] command = new String[] {"git", "--version"};
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java
index acb68cf..0fa89b5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java
@@ -14,7 +14,7 @@
 
 package com.googlesource.gerrit.plugins.replication.pull.fetch;
 
-import com.jcraft.jsch.JSchException;
+import org.apache.sshd.common.SshException;
 import org.eclipse.jgit.errors.TransportException;
 
 public class PermanentTransportException extends TransportException {
@@ -26,7 +26,8 @@
 
   public static TransportException wrapIfPermanentTransportException(TransportException e) {
     Throwable cause = e.getCause();
-    if (cause instanceof JSchException && cause.getMessage().startsWith("UnknownHostKey:")) {
+    if (cause instanceof SshException
+        && cause.getMessage().startsWith("Failed (UnsupportedCredentialItem) to execute:")) {
       return new PermanentTransportException("Terminal fetch failure", e);
     }
 
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 73c5c05..22b52e7 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -16,10 +16,6 @@
 group that is granted the 'Pull Replication' capability (provided
 by this plugin) or the 'Administrate Server' capability.
 
-When replicating hidden projects, the pull replication user needs to have
-the 'Administrate Server' capability or being added as the owner of each
-individual project that is supposed to be replicated.
-
 Change Indexing
 --------
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
index 4bc3105..dd9f98e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
@@ -725,8 +725,7 @@
     objectUnderTest.run();
 
     verify(pullReplicationApiRequestMetrics, never()).stop(any());
-    // TODO: To be enabled again on stable-3.6, where getTime() exists
-    // assertThat(testMetricMaker.getTimer("replication_latency")).isEqualTo(0);
+    assertThat(testMetricMaker.getTimer("replication_latency")).isEqualTo(0);
   }
 
   @Test
@@ -749,8 +748,7 @@
     objectUnderTest.run();
 
     verify(pullReplicationApiRequestMetrics).stop(any());
-    // TODO: To be enabled again on stable-3.6, where getTime() exists
-    // assertThat(testMetricMaker.getTimer("replication_latency")).isGreaterThan(0);
+    assertThat(testMetricMaker.getTimer("replication_latency")).isGreaterThan(0);
   }
 
   private void setupRequestScopeMock() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java
index 09a465c..fcb0702 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java
@@ -18,7 +18,7 @@
 
 import com.googlesource.gerrit.plugins.replication.pull.fetch.InexistentRefTransportException;
 import com.googlesource.gerrit.plugins.replication.pull.fetch.PermanentTransportException;
-import com.jcraft.jsch.JSchException;
+import org.apache.sshd.common.SshException;
 import org.eclipse.jgit.errors.TransportException;
 import org.junit.Test;
 
@@ -29,7 +29,9 @@
     assertThat(
             PermanentTransportException.wrapIfPermanentTransportException(
                 new TransportException(
-                    "SSH error", new JSchException("UnknownHostKey: some.place"))))
+                    "SSH error",
+                    new SshException(
+                        "Failed (UnsupportedCredentialItem) to execute: some.commands"))))
         .isInstanceOf(PermanentTransportException.class);
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
index 5cf01b1..0721dd2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
@@ -49,7 +49,8 @@
 @UseLocalDisk
 @TestPlugin(
     name = "pull-replication",
-    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule")
+    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
+    httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
 public class PullReplicationFanoutConfigIT extends LightweightPluginDaemonTest {
   private static final Optional<String> ALL_PROJECTS = Optional.empty();
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
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 18f7a0c..5717a8b 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
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 The Android Open Source Project
+// Copyright (C) 2020 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,9 +14,45 @@
 
 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 com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.SkipProjectClone;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
+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.events.HeadUpdatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
+import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+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.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Ignore;
+import org.junit.Test;
 
 @SkipProjectClone
 @UseLocalDisk
@@ -25,4 +61,350 @@
     sysModule =
         "com.googlesource.gerrit.plugins.replication.pull.PullReplicationITAbstract$PullReplicationTestModule",
     httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
-public class PullReplicationIT extends PullReplicationITAbstract {}
+public class PullReplicationIT extends PullReplicationSetupBase {
+
+  @Override
+  protected void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project)
+      throws IOException {
+    List<String> fetchUrls =
+        buildReplicaURLs(replicaSuffixes, s -> gitPath.resolve("${name}" + s + ".git").toString());
+    config.setStringList("remote", remoteName, "url", fetchUrls);
+    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
+    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
+    config.setInt("remote", remoteName, "timeout", 600);
+    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
+    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
+    config.setBoolean("gerrit", null, "autoReload", true);
+    config.save();
+  }
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    setUpTestPlugin(false);
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewChangeRef() throws Exception {
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewBranch() throws Exception {
+    String testProjectName = project + TEST_REPLICATION_SUFFIX;
+    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);
+    String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision;
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            ObjectId.zeroId().getName(),
+            branchRevision,
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project);
+        Repository sourceRepo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, newBranch) != null);
+
+      Ref targetBranchRef = getRef(repo, newBranch);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision);
+    }
+  }
+
+  @Test
+  @UseLocalDisk
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  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);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            ObjectId.zeroId().getName(),
+            branchRevision,
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(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);
+
+    FakeGitReferenceUpdatedEvent forcedPushEvent =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            branchRevision,
+            amendedCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(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
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewChangeRefCGitClient() throws Exception {
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+
+    config.setBoolean("replication", null, "useCGitClient", true);
+    config.save();
+
+    autoReloadConfigDecorator.reload();
+
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewBranchCGitClient() throws Exception {
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+
+    config.setBoolean("replication", null, "useCGitClient", true);
+    config.save();
+
+    autoReloadConfigDecorator.reload();
+
+    String testProjectName = project + TEST_REPLICATION_SUFFIX;
+    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);
+    String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision;
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            ObjectId.zeroId().getName(),
+            branchRevision,
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project);
+        Repository sourceRepo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, newBranch) != null);
+
+      Ref targetBranchRef = getRef(repo, newBranch);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision);
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldCreateNewProject() throws Exception {
+    NameKey projectToCreate = Project.nameKey(project.get() + "_created");
+
+    setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(projectToCreate.get()));
+    config.save();
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+    autoReloadConfigDecorator.reload();
+    Source source =
+        getInstance(SourcesCollection.class).getByRemoteName(TEST_REPLICATION_REMOTE).get();
+
+    FetchApiClient client = getInstance(FetchApiClient.Factory.class).create(source);
+    client.initProject(
+        projectToCreate,
+        new URIish(source.getApis().get(0)),
+        System.currentTimeMillis(),
+        Collections.emptyList());
+
+    waitUntil(() -> repoManager.list().contains(projectToCreate));
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateProjectDeletion() throws Exception {
+    String projectToDelete = project.get();
+    setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(projectToDelete));
+    config.save();
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+    autoReloadConfigDecorator.reload();
+
+    ProjectDeletedListener.Event event =
+        new ProjectDeletedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return projectToDelete;
+          }
+
+          @Override
+          public NotifyHandling getNotify() {
+            return NotifyHandling.NONE;
+          }
+        };
+    for (ProjectDeletedListener l : deletedListeners) {
+      l.onProjectDeleted(event);
+    }
+
+    waitUntil(() -> !repoManager.list().contains(project));
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateHeadUpdate() throws Exception {
+    String testProjectName = project.get();
+    setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(testProjectName));
+    config.save();
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+    autoReloadConfigDecorator.reload();
+
+    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);
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+
+    HeadUpdatedListener.Event event = new FakeHeadUpdateEvent(master, newBranch, testProjectName);
+    pullReplicationQueue.onHeadUpdated(event);
+
+    waitUntil(
+        () -> {
+          try {
+            return gApi.projects().name(testProjectName).head().equals(newBranch);
+          } catch (RestApiException e) {
+            return false;
+          }
+        });
+  }
+
+  @Ignore
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReplicateNewChangeRefToReplica() throws Exception {
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+}
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 1cffec7..a07aa55 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
@@ -33,6 +33,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -147,7 +148,8 @@
         .when(fetchRestApiClient.callSendObjects(any(), anyString(), anyLong(), any(), any()))
         .thenReturn(httpResult);
     when(fetchRestApiClient.callFetch(any(), anyString(), any())).thenReturn(fetchHttpResult);
-    when(fetchRestApiClient.initProject(any(), any())).thenReturn(successfulHttpResult);
+    when(fetchRestApiClient.initProject(any(), any(), anyLong(), any()))
+        .thenReturn(successfulHttpResult);
     when(successfulHttpResult.isSuccessful()).thenReturn(true);
     when(httpResult.isSuccessful()).thenReturn(true);
     when(fetchHttpResult.isSuccessful()).thenReturn(true);
@@ -206,7 +208,7 @@
     objectUnderTest.start();
     objectUnderTest.onEvent(event);
 
-    verify(fetchRestApiClient).initProject(any(), any());
+    verify(fetchRestApiClient).initProject(any(), any(), anyLong(), any());
   }
 
   @Test
@@ -219,7 +221,22 @@
     objectUnderTest.start();
     objectUnderTest.onEvent(event);
 
-    verify(fetchRestApiClient, never()).initProject(any(), any());
+    verify(fetchRestApiClient, never()).initProject(any(), any(), anyLong(), any());
+  }
+
+  @Test
+  public void shouldNotCallInitProjectWhenProjectWithoutConfiguration() throws Exception {
+    Event event = new TestEvent("refs/changes/01/1/meta");
+    when(httpResult.isSuccessful()).thenReturn(false);
+    when(httpResult.isProjectMissing(any())).thenReturn(true);
+    when(source.isCreateMissingRepositories()).thenReturn(true);
+    when(revReader.read(any(), any(), eq(RefNames.REFS_CONFIG), anyInt()))
+        .thenReturn(Optional.empty());
+
+    objectUnderTest.start();
+    objectUnderTest.onEvent(event);
+
+    verify(fetchRestApiClient, never()).initProject(any(), any(), anyLong(), any());
   }
 
   @Test
@@ -369,7 +386,7 @@
   }
 
   @Test
-  public void shouldCallDeleteWhenReplicateProjectDeletionsTrue() throws Exception {
+  public void shouldCallDeleteWhenReplicateProjectDeletionsTrue() {
     when(source.wouldDeleteProject(any())).thenReturn(true);
 
     String projectName = "testProject";
@@ -385,7 +402,7 @@
   }
 
   @Test
-  public void shouldNotCallDeleteWhenProjectNotToDelete() throws Exception {
+  public void shouldNotCallDeleteWhenProjectNotToDelete() {
     when(source.wouldDeleteProject(any())).thenReturn(false);
 
     FakeProjectDeletedEvent event = new FakeProjectDeletedEvent("testProject");
@@ -397,7 +414,7 @@
   }
 
   @Test
-  public void shouldScheduleUpdateHeadWhenWouldFetchProject() throws Exception {
+  public void shouldScheduleUpdateHeadWhenWouldFetchProject() {
     when(source.wouldFetchProject(any())).thenReturn(true);
 
     String projectName = "aProject";
@@ -413,7 +430,7 @@
   }
 
   @Test
-  public void shouldNotScheduleUpdateHeadWhenNotWouldFetchProject() throws Exception {
+  public void shouldNotScheduleUpdateHeadWhenNotWouldFetchProject() {
     when(source.wouldFetchProject(any())).thenReturn(false);
 
     String projectName = "aProject";
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 0a3960f..f8110d1 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
@@ -162,7 +162,7 @@
   }
 
   public ResponseHandler<Object> assertHttpResponseCode(int responseCode) {
-    return new ResponseHandler<Object>() {
+    return new ResponseHandler<>() {
 
       @Override
       public Object handleResponse(HttpResponse response)
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
index 213deec..e238528 100644
--- 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
@@ -106,7 +106,6 @@
             fetchStateLog,
             projectCache,
             sourceCollection,
-            applyObject,
             permissionBackend,
             eventDispatcherDataItem,
             new LocalGitRepositoryManagerProvider(gitManager));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
index 10415e4..cf7515e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
@@ -94,10 +94,11 @@
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
-  public void shouldReturnForbiddenForUserWithoutPermissionsOnReplica() throws Exception {
+  public void shouldReturnUnauthorizedForUserWithoutPermissionsOnReplica() throws Exception {
     httpClientFactory
         .create(source)
-        .execute(createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN));
+        .execute(
+            createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED));
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
index c543969..0f13881 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
@@ -184,12 +184,13 @@
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
-  public void shouldReturnForbiddenForUserWithoutPermissionsWhenNodeIsAReplica() throws Exception {
+  public void shouldReturnUnauthorizedForUserWithoutPermissionsWhenNodeIsAReplica()
+      throws Exception {
     httpClientFactory
         .create(source)
         .execute(
             createPutRequestWithHeaders(),
-            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN));
+            assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED));
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
index 9e5ca8d..9492d49 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -4,18 +4,25 @@
 import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY;
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.internal.verification.VerificationModeFactory.atLeastOnce;
 import static org.mockito.internal.verification.VerificationModeFactory.times;
 
 import com.google.common.net.MediaType;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.*;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.ProjectsCollection;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.util.Providers;
 import java.io.*;
 import java.nio.charset.StandardCharsets;
+import java.util.Optional;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
@@ -37,10 +44,12 @@
   @Mock private ProjectInitializationAction projectInitializationAction;
   @Mock private UpdateHeadAction updateHEADAction;
   @Mock private ProjectDeletionAction projectDeletionAction;
-  @Mock private ProjectsCollection projectsCollection;
-  @Mock private ProjectResource projectResource;
+  @Mock private ProjectCache projectCache;
+  @Mock private ProjectState projectState;
   @Mock private ServletOutputStream outputStream;
   @Mock private PrintWriter printWriter;
+  @Mock private IdentifiedUser identifiedUserMock;
+  @Mock private AnonymousUser anonymousUserMock;
   private final String PLUGIN_NAME = "pull-replication";
   private final String PROJECT_NAME = "some-project";
   private final String PROJECT_NAME_GIT = "some-project.git";
@@ -60,6 +69,10 @@
   private final Response OK_RESPONSE = Response.ok();
 
   private PullReplicationFilter createPullReplicationFilter() {
+    return createPullReplicationFilter(identifiedUserMock);
+  }
+
+  private PullReplicationFilter createPullReplicationFilter(CurrentUser currentUser) {
     return new PullReplicationFilter(
         fetchAction,
         applyObjectAction,
@@ -67,8 +80,9 @@
         projectInitializationAction,
         updateHEADAction,
         projectDeletionAction,
-        projectsCollection,
-        PLUGIN_NAME);
+        projectCache,
+        PLUGIN_NAME,
+        Providers.of(currentUser));
   }
 
   private void defineBehaviours(byte[] payload, String uri) throws Exception {
@@ -76,15 +90,14 @@
     InputStream is = new ByteArrayInputStream(payload);
     BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
     when(request.getReader()).thenReturn(bufferedReader);
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
     when(response.getWriter()).thenReturn(printWriter);
   }
 
   private void verifyBehaviours() throws Exception {
     verify(request, atLeastOnce()).getRequestURI();
     verify(request).getReader();
-    verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME));
+    verify(projectCache).get(Project.nameKey(PROJECT_NAME));
     verify(response).getWriter();
     verify(response).setContentType("application/json");
     verify(response).setStatus(HttpServletResponse.SC_OK);
@@ -107,7 +120,7 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(fetchAction).apply(eq(projectResource), any());
+    verify(fetchAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
@@ -130,7 +143,7 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(applyObjectAction).apply(eq(projectResource), any());
+    verify(applyObjectAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
@@ -152,7 +165,7 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(applyObjectsAction).apply(eq(projectResource), any());
+    verify(applyObjectsAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
@@ -160,15 +173,11 @@
 
     when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI);
     when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString());
-    when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(true);
-    when(response.getWriter()).thenReturn(printWriter);
 
     final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
     pullReplicationFilter.doFilter(request, response, filterChain);
 
-    verify(request, times(5)).getRequestURI();
-    verify(projectInitializationAction).initProject(eq(PROJECT_NAME_GIT));
-    verify(response).getWriter();
+    verify(projectInitializationAction).service(request, response);
   }
 
   @Test
@@ -183,15 +192,14 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(updateHEADAction).apply(eq(projectResource), any());
+    verify(updateHEADAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
   public void shouldFilterProjectDeletionAction() throws Exception {
     when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI);
     when(request.getMethod()).thenReturn("DELETE");
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
     when(projectDeletionAction.apply(any(), any())).thenReturn(OK_RESPONSE);
     when(response.getWriter()).thenReturn(printWriter);
 
@@ -199,8 +207,8 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verify(request, times(7)).getRequestURI();
-    verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME));
-    verify(projectDeletionAction).apply(eq(projectResource), any());
+    verify(projectCache).get(Project.nameKey(PROJECT_NAME));
+    verify(projectDeletionAction).apply(any(ProjectResource.class), any());
     verify(response).getWriter();
     verify(response).setContentType("application/json");
     verify(response).setStatus(OK_RESPONSE.statusCode());
@@ -215,6 +223,17 @@
   }
 
   @Test
+  public void shouldGoNextInChainWhenAnonymousRequestUriDoesNotMatch() throws Exception {
+    when(request.getRequestURI()).thenReturn("any-url");
+    lenient().when(response.getOutputStream()).thenReturn(outputStream);
+
+    final PullReplicationFilter pullReplicationFilter =
+        createPullReplicationFilter(anonymousUserMock);
+    pullReplicationFilter.doFilter(request, response, filterChain);
+    verify(filterChain).doFilter(request, response);
+  }
+
+  @Test
   public void shouldBe404WhenJsonIsMalformed() throws Exception {
     byte[] payloadMalformedJson = "some-json-malformed".getBytes(StandardCharsets.UTF_8);
     InputStream is = new ByteArrayInputStream(payloadMalformedJson);
@@ -230,24 +249,10 @@
   }
 
   @Test
-  public void shouldBe500WhenProjectCannotBeInitiated() throws Exception {
-    when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI);
-    when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString());
-    when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(false);
-    when(response.getOutputStream()).thenReturn(outputStream);
-
-    final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
-    pullReplicationFilter.doFilter(request, response, filterChain);
-
-    verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-  }
-
-  @Test
   public void shouldBe500WhenResourceNotFound() throws Exception {
     when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI);
     when(request.getMethod()).thenReturn("DELETE");
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
     when(projectDeletionAction.apply(any(), any()))
         .thenThrow(new ResourceNotFoundException("resource not found"));
     when(response.getOutputStream()).thenReturn(outputStream);
@@ -280,6 +285,19 @@
   }
 
   @Test
+  public void shouldBe401WhenUserIsAnonymous() throws Exception {
+    byte[] payloadFetchAction = "{}".getBytes(StandardCharsets.UTF_8);
+
+    defineBehaviours(payloadFetchAction, FETCH_URI);
+    when(response.getOutputStream()).thenReturn(outputStream);
+
+    PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(anonymousUserMock);
+    pullReplicationFilter.doFilter(request, response, filterChain);
+
+    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+  }
+
+  @Test
   public void shouldBe422WhenEntityCannotBeProcessed() throws Exception {
     byte[] payloadFetchAction =
         ("{"
@@ -304,8 +322,7 @@
   public void shouldBe409WhenThereIsResourceConflict() throws Exception {
     when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI);
     when(request.getMethod()).thenReturn("DELETE");
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
 
     when(projectDeletionAction.apply(any(), any()))
         .thenThrow(new ResourceConflictException("Resource conflict"));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
index 25aa2a7..65ccdfb 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
@@ -24,6 +24,7 @@
 import com.google.common.base.Charsets;
 import com.google.common.collect.Lists;
 import com.google.common.io.CharStreams;
+import com.google.common.net.MediaType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
@@ -37,6 +38,7 @@
 import java.nio.ByteBuffer;
 import java.util.Collections;
 import org.apache.http.Header;
+import org.apache.http.HttpHeaders;
 import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
@@ -94,6 +96,17 @@
           + "\",\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"sha1\":\""
           + blobObjectId
           + "\",\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}}";
+
+  String expectedInitProjectWithConfigPayload =
+      "{\"label\":\"Replication\",\"ref_name\":\"refs/meta/config\",\"event_created_on\":"
+          + eventCreatedOn
+          + ",\"revisions_data\":[{\"commit_object\":{\"sha1\":\""
+          + commitObjectId
+          + "\",\"type\":1,\"content\":\"dHJlZSA3NzgxNGQyMTZhNmNhYjJkZGI5ZjI4NzdmYmJkMGZlYmRjMGZhNjA4CnBhcmVudCA5ODNmZjFhM2NmNzQ3MjVhNTNhNWRlYzhkMGMwNjEyMjEyOGY1YThkCmF1dGhvciBHZXJyaXQgVXNlciAxMDAwMDAwIDwxMDAwMDAwQDY5ZWMzOGYwLTM1MGUtNGQ5Yy05NmQ0LWJjOTU2ZjJmYWFhYz4gMTYxMDU3ODY0OCArMDEwMApjb21taXR0ZXIgR2Vycml0IENvZGUgUmV2aWV3IDxyb290QG1hY3plY2gtWFBTLTE1PiAxNjEwNTc4NjQ4ICswMTAwCgpVcGRhdGUgcGF0Y2ggc2V0IDEKClBhdGNoIFNldCAxOgoKKDEgY29tbWVudCkKClBhdGNoLXNldDogMQo\\u003d\"},\"tree_object\":{\"sha1\":\""
+          + treeObjectId
+          + "\",\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"sha1\":\""
+          + blobObjectId
+          + "\",\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}]}";
   String commitObject =
       "tree "
           + treeObjectId
@@ -374,18 +387,26 @@
 
   @Test
   public void shouldCallInitProjectEndpoint() throws Exception {
-
-    objectUnderTest.initProject(Project.nameKey("test_repo"), new URIish(api));
+    objectUnderTest.initProject(
+        Project.nameKey("test_repo"),
+        new URIish(api),
+        eventCreatedOn,
+        Collections.singletonList(createSampleRevisionData()));
 
     verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any());
-
     HttpPut httpPut = httpPutCaptor.getValue();
+    String payload =
+        CharStreams.toString(
+            new InputStreamReader(httpPut.getEntity().getContent(), Charsets.UTF_8));
     assertThat(httpPut.getURI().getHost()).isEqualTo("gerrit-host");
+    assertThat(httpPut.getHeaders(HttpHeaders.CONTENT_TYPE)[0].getValue())
+        .isEqualTo(MediaType.JSON_UTF_8.toString());
     assertThat(httpPut.getURI().getPath())
         .isEqualTo(
             String.format(
                 "%s/plugins/pull-replication/init-project/test_repo.git",
                 urlAuthenticationPrefix()));
+    assertThat(payload).isEqualTo(expectedInitProjectWithConfigPayload);
     assertAuthentication(httpPut);
   }