Merge remote-tracking branch 'origin/stable-3.3' into stable-3.4

* origin/stable-3.3:
  Allow project deletion on replicas
  Allow project creation only when CREATE_PROJECT capability is set
  Allow dynamic url creation in tests
  Handle HEAD update in replicas
  Trigger remote update HEAD
  Allow project deletion on primaries

Change-Id: Ib912152066c56b9ea382c6bb737ad6d8d9e22a77
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/DeleteProjectTask.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/DeleteProjectTask.java
new file mode 100644
index 0000000..ed584b7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/DeleteProjectTask.java
@@ -0,0 +1,79 @@
+// 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;
+
+import static com.googlesource.gerrit.plugins.replication.pull.ReplicationQueue.repLog;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.ioutil.HexFormat;
+import com.google.gerrit.server.util.IdGenerator;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
+import com.googlesource.gerrit.plugins.replication.pull.client.HttpResult;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import org.eclipse.jgit.transport.URIish;
+
+public class DeleteProjectTask implements Runnable {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  interface Factory {
+    DeleteProjectTask create(Source source, String uri, Project.NameKey project);
+  }
+
+  private final int id;
+  private final Source source;
+  private final String uri;
+  private final Project.NameKey project;
+  private final FetchRestApiClient.Factory fetchClientFactory;
+
+  @Inject
+  DeleteProjectTask(
+      FetchRestApiClient.Factory fetchClientFactory,
+      IdGenerator ig,
+      @Assisted Source source,
+      @Assisted String uri,
+      @Assisted Project.NameKey project) {
+    this.fetchClientFactory = fetchClientFactory;
+    this.id = ig.next();
+    this.uri = uri;
+    this.source = source;
+    this.project = project;
+  }
+
+  @Override
+  public void run() {
+    try {
+      URIish urIish = new URIish(uri);
+      HttpResult httpResult = fetchClientFactory.create(source).deleteProject(project, urIish);
+      if (!httpResult.isSuccessful()) {
+        throw new IOException(httpResult.getMessage().orElse("Unknown"));
+      }
+      logger.atFine().log("Successfully deleted project {} on remote {}", project.get(), uri);
+    } catch (URISyntaxException | IOException e) {
+      String errorMessage =
+          String.format("Cannot delete project %s on remote site %s.", project, uri);
+      logger.atWarning().withCause(e).log(errorMessage);
+      repLog.warn(errorMessage);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return String.format("[%s] delete-project %s at %s", HexFormat.fromInt(id), project.get(), uri);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/GerritConfigOps.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/GerritConfigOps.java
new file mode 100644
index 0000000..2437d80
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/GerritConfigOps.java
@@ -0,0 +1,54 @@
+// 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;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.URIish;
+
+@Singleton
+public class GerritConfigOps {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SitePaths sitePath;
+  private final Config gerritConfig;
+
+  @Inject
+  public GerritConfigOps(@GerritServerConfig Config cfg, SitePaths sitePath) {
+    this.sitePath = sitePath;
+    this.gerritConfig = cfg;
+  }
+
+  public Optional<URIish> getGitRepositoryURI(String projectName) {
+    Path basePath = sitePath.resolve(gerritConfig.getString("gerrit", null, "basePath"));
+    URIish uri;
+
+    try {
+      uri = new URIish("file://" + basePath + "/" + projectName);
+      return Optional.of(uri);
+    } catch (URISyntaxException e) {
+      logger.atSevere().withCause(e).log("Unsupported URI for project " + projectName);
+    }
+
+    return Optional.empty();
+  }
+}
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 50ab913..29ec93a 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
@@ -20,7 +20,9 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.EventTypes;
@@ -101,6 +103,8 @@
 
     bind(EventBus.class).in(Scopes.SINGLETON);
     bind(ReplicationSources.class).to(SourcesCollection.class);
+    DynamicSet.bind(binder(), ProjectDeletedListener.class).to(ReplicationQueue.class);
+    DynamicSet.bind(binder(), HeadUpdatedListener.class).to(ReplicationQueue.class);
 
     bind(ReplicationQueue.class).in(Scopes.SINGLETON);
     bind(ObservableQueue.class).to(ReplicationQueue.class);
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 2a77e09..0e4ace1 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
@@ -19,7 +19,9 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.events.EventDispatcher;
 import com.google.gerrit.server.git.WorkQueue;
@@ -51,7 +53,11 @@
 import org.slf4j.LoggerFactory;
 
 public class ReplicationQueue
-    implements ObservableQueue, LifecycleListener, GitReferenceUpdatedListener {
+    implements ObservableQueue,
+        LifecycleListener,
+        GitReferenceUpdatedListener,
+        ProjectDeletedListener,
+        HeadUpdatedListener {
 
   static final String PULL_REPLICATION_LOG_NAME = "pull_replication_log";
   static final Logger repLog = LoggerFactory.getLogger(PULL_REPLICATION_LOG_NAME);
@@ -131,6 +137,16 @@
     }
   }
 
+  @Override
+  public void onProjectDeleted(ProjectDeletedListener.Event event) {
+    Project.NameKey project = Project.nameKey(event.getProjectName());
+    sources.get().getAll().stream()
+        .filter((Source s) -> s.wouldDeleteProject(project))
+        .forEach(
+            source ->
+                source.getApis().forEach(apiUrl -> source.scheduleDeleteProject(apiUrl, project)));
+  }
+
   private Boolean isRefToBeReplicated(String refName) {
     return !refsFilter.match(refName);
   }
@@ -298,6 +314,17 @@
     }
   }
 
+  @Override
+  public void onHeadUpdated(HeadUpdatedListener.Event event) {
+    Project.NameKey p = Project.nameKey(event.getProjectName());
+    sources.get().getAll().stream()
+        .filter(s -> s.wouldFetchProject(p))
+        .forEach(
+            s ->
+                s.getApis()
+                    .forEach(apiUrl -> s.scheduleUpdateHead(apiUrl, p, event.getNewHeadName())));
+  }
+
   @AutoValue
   abstract static class ReferenceUpdatedEvent {
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
index 45713b8..28aa541 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
@@ -70,6 +70,7 @@
 import com.googlesource.gerrit.plugins.replication.pull.fetch.JGitFetch;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.util.HashMap;
 import java.util.List;
@@ -80,6 +81,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 import org.apache.commons.io.FilenameUtils;
@@ -101,6 +103,7 @@
   }
 
   private final ReplicationStateListener stateLog;
+  private final UpdateHeadTask.Factory updateHeadFactory;
   private final Object stateLock = new Object();
   private final Map<URIish, FetchOne> pending = new HashMap<>();
   private final Map<URIish, FetchOne> inFlight = new HashMap<>();
@@ -114,6 +117,7 @@
   private final SourceConfiguration config;
   private final DynamicItem<EventDispatcher> eventDispatcher;
   private CloseableHttpClient httpClient;
+  private final DeleteProjectTask.Factory deleteProjectFactory;
 
   protected enum RetryReason {
     TRANSPORT_ERROR,
@@ -180,6 +184,7 @@
                 bind(Source.class).toInstance(Source.this);
                 bind(SourceConfiguration.class).toInstance(config);
                 install(new FactoryModuleBuilder().build(FetchOne.Factory.class));
+                install(new FactoryModuleBuilder().build(DeleteProjectTask.Factory.class));
                 Class<? extends Fetch> clientClass =
                     cfg.useCGitClient() ? CGitFetch.class : JGitFetch.class;
                 install(
@@ -187,6 +192,7 @@
                         .implement(Fetch.class, BatchFetchClient.class)
                         .implement(Fetch.class, FetchClientImplementation.class, clientClass)
                         .build(FetchFactory.class));
+                factory(UpdateHeadTask.Factory.class);
               }
 
               @Provides
@@ -210,6 +216,8 @@
     child.getBinding(FetchFactory.class).acceptTargetVisitor(new CGitFetchValidator());
     opFactory = child.getInstance(FetchOne.Factory.class);
     threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
+    deleteProjectFactory = child.getInstance(DeleteProjectTask.Factory.class);
+    updateHeadFactory = child.getInstance(UpdateHeadTask.Factory.class);
   }
 
   public synchronized CloseableHttpClient memoize(
@@ -445,6 +453,12 @@
     }
   }
 
+  void scheduleDeleteProject(String uri, Project.NameKey project) {
+    @SuppressWarnings("unused")
+    ScheduledFuture<?> ignored =
+        pool.schedule(deleteProjectFactory.create(this, uri, project), 0, TimeUnit.SECONDS);
+  }
+
   void fetchWasCanceled(FetchOne fetchOp) {
     synchronized (stateLock) {
       URIish uri = fetchOp.getURI();
@@ -592,11 +606,22 @@
     return false;
   }
 
+  public boolean wouldDeleteProject(Project.NameKey project) {
+    if (isReplicateProjectDeletions()) {
+      return configSettingsAllowReplication(project);
+    }
+    return false;
+  }
+
   public boolean wouldFetchProject(Project.NameKey project) {
     if (!shouldReplicate(project)) {
       return false;
     }
 
+    return configSettingsAllowReplication(project);
+  }
+
+  private boolean configSettingsAllowReplication(Project.NameKey project) {
     // by default fetch all projects
     List<String> projects = config.getProjects();
     if (projects.isEmpty()) {
@@ -737,6 +762,23 @@
     return config.createMissingRepositories();
   }
 
+  public boolean isReplicateProjectDeletions() {
+    return config.replicateProjectDeletions();
+  }
+
+  void scheduleUpdateHead(String apiUrl, Project.NameKey project, String newHead) {
+    try {
+      URIish apiURI = new URIish(apiUrl);
+      @SuppressWarnings("unused")
+      ScheduledFuture<?> ignored =
+          pool.schedule(
+              updateHeadFactory.create(this, apiURI, project, newHead), 0, TimeUnit.SECONDS);
+    } catch (URISyntaxException e) {
+      logger.atSevere().withCause(e).log(
+          "Could not schedule HEAD pull-replication for project {}", project.get());
+    }
+  }
+
   private static boolean matches(URIish uri, String urlMatch) {
     if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
       return true;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
index 4858b17..ab9c634 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
@@ -39,6 +39,7 @@
   private final boolean replicatePermissions;
   private final boolean replicateHiddenProjects;
   private final boolean createMissingRepositories;
+  private final boolean replicateProjectDeletions;
   private final String remoteNameStyle;
   private final ImmutableList<String> urls;
   private final ImmutableList<String> projects;
@@ -76,6 +77,7 @@
     lockErrorMaxRetries = cfg.getInt("replication", "lockErrorMaxRetries", 0);
 
     createMissingRepositories = cfg.getBoolean("remote", name, "createMissingRepositories", true);
+    replicateProjectDeletions = cfg.getBoolean("remote", name, "replicateProjectDeletions", true);
     replicatePermissions = cfg.getBoolean("remote", name, "replicatePermissions", true);
     replicateHiddenProjects = cfg.getBoolean("remote", name, "replicateHiddenProjects", false);
     useCGitClient = cfg.getBoolean("replication", "useCGitClient", false);
@@ -197,6 +199,10 @@
     return createMissingRepositories;
   }
 
+  public boolean replicateProjectDeletions() {
+    return replicateProjectDeletions;
+  }
+
   private static int getInt(RemoteConfig rc, Config cfg, String name, int defValue) {
     return cfg.getInt("remote", rc.getName(), name, defValue);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/UpdateHeadTask.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/UpdateHeadTask.java
new file mode 100644
index 0000000..943ea92
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/UpdateHeadTask.java
@@ -0,0 +1,87 @@
+// 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;
+
+import static com.googlesource.gerrit.plugins.replication.pull.ReplicationQueue.repLog;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.ioutil.HexFormat;
+import com.google.gerrit.server.util.IdGenerator;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
+import com.googlesource.gerrit.plugins.replication.pull.client.HttpResult;
+import java.io.IOException;
+import org.eclipse.jgit.transport.URIish;
+
+public class UpdateHeadTask implements Runnable {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final FetchRestApiClient.Factory fetchClientFactory;
+  private final Source source;
+  private final URIish apiURI;
+  private final Project.NameKey project;
+  private final String newHead;
+  private final int id;
+
+  interface Factory {
+    UpdateHeadTask create(Source source, URIish apiURI, Project.NameKey project, String newHead);
+  }
+
+  @Inject
+  UpdateHeadTask(
+      FetchRestApiClient.Factory fetchClientFactory,
+      IdGenerator ig,
+      @Assisted Source source,
+      @Assisted URIish apiURI,
+      @Assisted Project.NameKey project,
+      @Assisted String newHead) {
+    this.fetchClientFactory = fetchClientFactory;
+    this.id = ig.next();
+    this.source = source;
+    this.apiURI = apiURI;
+    this.project = project;
+    this.newHead = newHead;
+  }
+
+  @Override
+  public void run() {
+    try {
+      HttpResult httpResult =
+          fetchClientFactory.create(source).updateHead(project, newHead, apiURI);
+      if (!httpResult.isSuccessful()) {
+        throw new IOException(httpResult.getMessage().orElse("Unknown"));
+      }
+      logger.atFine().log(
+          "Successfully updated HEAD of project {} on remote {}",
+          project.get(),
+          apiURI.toASCIIString());
+    } catch (IOException e) {
+      String errorMessage =
+          String.format(
+              "Cannot update HEAD of project %s remote site %s",
+              project.get(), apiURI.toASCIIString());
+      logger.atWarning().withCause(e).log(errorMessage);
+      repLog.warn(errorMessage);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "[%s] update-head %s at %s to %s",
+        HexFormat.fromInt(id), project.get(), apiURI.toASCIIString(), newHead);
+  }
+}
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
new file mode 100644
index 0000000..8915e78
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
@@ -0,0 +1,68 @@
+// 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;
+
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+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;
+
+class ProjectDeletionAction
+    implements RestModifyView<ProjectResource, ProjectDeletionAction.DeleteInput> {
+  private static final PluginPermission DELETE_PROJECT =
+      new PluginPermission("delete-project", "deleteProject");
+
+  static class DeleteInput {}
+
+  private final GerritConfigOps gerritConfigOps;
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  ProjectDeletionAction(GerritConfigOps gerritConfigOps, PermissionBackend permissionBackend) {
+    this.gerritConfigOps = gerritConfigOps;
+    this.permissionBackend = permissionBackend;
+  }
+
+  @Override
+  public Response<?> apply(ProjectResource projectResource, DeleteInput input)
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
+
+    permissionBackend.user(projectResource.getUser()).check(DELETE_PROJECT);
+
+    Optional<URIish> maybeRepoURI =
+        gerritConfigOps.getGitRepositoryURI(String.format("%s.git", projectResource.getName()));
+
+    if (maybeRepoURI.isPresent()) {
+      if (new LocalFS(maybeRepoURI.get()).deleteProject(projectResource.getNameKey())) {
+        return Response.ok();
+      }
+      throw new UnprocessableEntityException(
+          String.format("Could not delete project %s", projectResource.getName()));
+    }
+    throw new ResourceNotFoundException(
+        String.format("Could not compute URI for repo: %s", projectResource.getName()));
+  }
+}
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 8f5c9d0..01635e6 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
@@ -21,23 +21,23 @@
 import com.google.common.flogger.FluentLogger;
 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.Url;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 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.io.IOException;
-import java.net.URISyntaxException;
-import java.nio.file.Path;
 import java.util.Optional;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.URIish;
 
 @Singleton
@@ -47,16 +47,18 @@
 
   public static final String PROJECT_NAME = "project-name";
 
-  private final SitePaths sitePath;
-  private final Config gerritConfig;
+  private final GerritConfigOps gerritConfigOps;
   private final Provider<CurrentUser> userProvider;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   ProjectInitializationAction(
-      @GerritServerConfig Config cfg, SitePaths sitePath, Provider<CurrentUser> userProvider) {
-    this.sitePath = sitePath;
-    this.gerritConfig = cfg;
+      GerritConfigOps gerritConfigOps,
+      Provider<CurrentUser> userProvider,
+      PermissionBackend permissionBackend) {
+    this.gerritConfigOps = gerritConfigOps;
     this.userProvider = userProvider;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -79,11 +81,19 @@
     String path = httpServletRequest.getRequestURI();
     String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1));
 
-    if (initProject(projectName)) {
+    try {
+      if (initProject(projectName)) {
+        setResponse(
+            httpServletResponse,
+            HttpServletResponse.SC_CREATED,
+            "Project " + projectName + " initialized");
+        return;
+      }
+    } catch (AuthException | PermissionBackendException e) {
       setResponse(
           httpServletResponse,
-          HttpServletResponse.SC_CREATED,
-          "Project " + projectName + " initialized");
+          HttpServletResponse.SC_FORBIDDEN,
+          "User not authorized to create project " + projectName);
       return;
     }
 
@@ -93,8 +103,11 @@
         "Cannot initialize project " + projectName);
   }
 
-  protected boolean initProject(String projectName) {
-    Optional<URIish> maybeUri = getGitRepositoryURI(projectName);
+  protected boolean initProject(String projectName)
+      throws AuthException, PermissionBackendException {
+    permissionBackend.user(userProvider.get()).check(GlobalPermission.CREATE_PROJECT);
+
+    Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(projectName);
     if (!maybeUri.isPresent()) {
       logger.atSevere().log("Cannot initialize project '{}'", projectName);
       return false;
@@ -104,20 +117,6 @@
     return localFS.createProject(projectNameKey, RefNames.HEAD);
   }
 
-  private Optional<URIish> getGitRepositoryURI(String projectName) {
-    Path basePath = sitePath.resolve(gerritConfig.getString("gerrit", null, "basePath"));
-    URIish uri;
-
-    try {
-      uri = new URIish("file://" + basePath + "/" + projectName);
-      return Optional.of(uri);
-    } catch (URISyntaxException e) {
-      logger.atSevere().withCause(e).log("Unsupported URI for project " + projectName);
-    }
-
-    return Optional.empty();
-  }
-
   public static String getProjectInitializationUrl(String pluginName, String projectName) {
     return String.format(
         "a/plugins/%s/init-project/%s", pluginName, Url.encode(projectName) + ".git");
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 1425be6..44dd3bb 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
@@ -27,6 +27,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.projects.HeadInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -74,6 +75,8 @@
   private FetchAction fetchAction;
   private ApplyObjectAction applyObjectAction;
   private ProjectInitializationAction projectInitializationAction;
+  private UpdateHeadAction updateHEADAction;
+  private ProjectDeletionAction projectDeletionAction;
   private ProjectsCollection projectsCollection;
   private Gson gson;
   private Provider<CurrentUser> userProvider;
@@ -83,11 +86,15 @@
       FetchAction fetchAction,
       ApplyObjectAction applyObjectAction,
       ProjectInitializationAction projectInitializationAction,
+      UpdateHeadAction updateHEADAction,
+      ProjectDeletionAction projectDeletionAction,
       ProjectsCollection projectsCollection,
       Provider<CurrentUser> userProvider) {
     this.fetchAction = fetchAction;
     this.applyObjectAction = applyObjectAction;
     this.projectInitializationAction = projectInitializationAction;
+    this.updateHEADAction = updateHEADAction;
+    this.projectDeletionAction = projectDeletionAction;
     this.projectsCollection = projectsCollection;
     this.userProvider = userProvider;
     this.gson = OutputFormat.JSON.newGsonBuilder().create();
@@ -125,6 +132,18 @@
         } else {
           httpResponse.sendError(SC_UNAUTHORIZED);
         }
+      } else if (isUpdateHEADAction(httpRequest)) {
+        if (userProvider.get().isIdentifiedUser()) {
+          writeResponse(httpResponse, doUpdateHEAD(httpRequest));
+        } else {
+          httpResponse.sendError(SC_UNAUTHORIZED);
+        }
+      } else if (isDeleteProjectAction(httpRequest)) {
+        if (userProvider.get().isIdentifiedUser()) {
+          writeResponse(httpResponse, doDeleteProject(httpRequest));
+        } else {
+          httpResponse.sendError(SC_UNAUTHORIZED);
+        }
       } else {
         chain.doFilter(request, response);
       }
@@ -153,7 +172,7 @@
   }
 
   private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, PermissionBackendException {
 
     String path = httpRequest.getRequestURI();
     String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1));
@@ -176,6 +195,23 @@
   }
 
   @SuppressWarnings("unchecked")
+  private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception {
+    HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class));
+    ProjectResource projectResource =
+        projectsCollection.parse(TopLevelResource.INSTANCE, getProjectName(httpRequest));
+
+    return (Response<String>) updateHEADAction.apply(projectResource, input);
+  }
+
+  @SuppressWarnings("unchecked")
+  private Response<String> doDeleteProject(HttpServletRequest httpRequest) throws Exception {
+    ProjectResource projectResource =
+        projectsCollection.parse(TopLevelResource.INSTANCE, getProjectName(httpRequest));
+    return (Response<String>)
+        projectDeletionAction.apply(projectResource, 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));
@@ -185,8 +221,8 @@
     return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input);
   }
 
-  private void writeResponse(
-      HttpServletResponse httpResponse, Response<Map<String, Object>> response) throws IOException {
+  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) {
 
@@ -260,4 +296,14 @@
   private boolean isInitProjectAction(HttpServletRequest httpRequest) {
     return httpRequest.getRequestURI().contains("pull-replication/init-project/");
   }
+
+  private boolean isUpdateHEADAction(HttpServletRequest httpRequest) {
+    return httpRequest.getRequestURI().matches("(/a)?/projects/[^/]+/HEAD")
+        && "PUT".equals(httpRequest.getMethod());
+  }
+
+  private boolean isDeleteProjectAction(HttpServletRequest httpRequest) {
+    return httpRequest.getRequestURI().matches("(/a)?/projects/[^/]+$")
+        && "DELETE".equals(httpRequest.getMethod());
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
new file mode 100644
index 0000000..4195435
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
@@ -0,0 +1,79 @@
+// 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;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.projects.HeadInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+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
+public class UpdateHeadAction implements RestModifyView<ProjectResource, HeadInput> {
+  private final GerritConfigOps gerritConfigOps;
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  UpdateHeadAction(GerritConfigOps gerritConfigOps, PermissionBackend permissionBackend) {
+    this.gerritConfigOps = gerritConfigOps;
+    this.permissionBackend = permissionBackend;
+  }
+
+  @Override
+  public Response<?> apply(ProjectResource projectResource, HeadInput input)
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
+    if (input == null || Strings.isNullOrEmpty(input.ref)) {
+      throw new BadRequestException("ref required");
+    }
+    String ref = RefNames.fullName(input.ref);
+
+    permissionBackend
+        .user(projectResource.getUser())
+        .project(projectResource.getNameKey())
+        .ref(ref)
+        .check(RefPermission.SET_HEAD);
+
+    // TODO: the .git suffix should not be added here, but rather it should be
+    //  dealt with by the caller, honouring the naming style from the
+    //  replication.config (Issue 15221)
+    Optional<URIish> maybeRepo =
+        gerritConfigOps.getGitRepositoryURI(String.format("%s.git", projectResource.getName()));
+
+    if (maybeRepo.isPresent()) {
+      if (new LocalFS(maybeRepo.get()).updateHead(projectResource.getNameKey(), ref)) {
+        return Response.ok(ref);
+      }
+      throw new UnprocessableEntityException(
+          String.format(
+              "Could not update HEAD of repo %s to ref %s", projectResource.getName(), ref));
+    }
+    throw new ResourceNotFoundException(
+        String.format("Could not compute URL for repo: %s", projectResource.getName()));
+  }
+}
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 811d064..df97609 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
@@ -44,6 +44,7 @@
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.CredentialsProvider;
 import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.protocol.HttpClientContext;
@@ -121,6 +122,25 @@
     return httpClientFactory.create(source).execute(put, this, getContext(uri));
   }
 
+  public HttpResult deleteProject(Project.NameKey project, URIish apiUri) throws IOException {
+    String url =
+        String.format("%s/%s", apiUri.toASCIIString(), getProjectDeletionUrl(project.get()));
+    HttpDelete delete = new HttpDelete(url);
+    return httpClientFactory.create(source).execute(delete, this, getContext(apiUri));
+  }
+
+  public HttpResult updateHead(Project.NameKey project, String newHead, URIish apiUri)
+      throws IOException {
+    logger.atFine().log("Updating head of %s on %s", project.get(), newHead);
+    String url =
+        String.format("%s/%s", apiUri.toASCIIString(), getProjectUpdateHeadUrl(project.get()));
+    HttpPut req = new HttpPut(url);
+    req.setEntity(
+        new StringEntity(String.format("{\"ref\": \"%s\"}", newHead), StandardCharsets.UTF_8));
+    req.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString()));
+    return httpClientFactory.create(source).execute(req, this, getContext(apiUri));
+  }
+
   public HttpResult callSendObject(
       Project.NameKey project, String refName, RevisionData revisionData, URIish targetUri)
       throws ClientProtocolException, IOException {
@@ -168,4 +188,12 @@
     }
     return null;
   }
+
+  String getProjectDeletionUrl(String projectName) {
+    return String.format("a/projects/%s", Url.encode(projectName));
+  }
+
+  String getProjectUpdateHeadUrl(String projectName) {
+    return String.format("a/projects/%s/HEAD", Url.encode(projectName));
+  }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f2a596b..68c9001 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -354,6 +354,12 @@
 
 	By default, true.
 
+remote.NAME.replicateProjectDeletions
+:	If true, project deletions will also be replicated to the
+remote site.
+
+	By default, false, do *not* replicate project deletions.
+
 remote.NAME.authGroup
 :	Specifies the name of a group that the remote should use to
 	access the repositories. Multiple authGroups may be specified
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FakeHeadUpdateEvent.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FakeHeadUpdateEvent.java
new file mode 100644
index 0000000..d1769ab
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FakeHeadUpdateEvent.java
@@ -0,0 +1,52 @@
+// 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;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
+
+class FakeHeadUpdateEvent implements HeadUpdatedListener.Event {
+
+  private final String oldName;
+  private final String newName;
+  private final String projectName;
+
+  FakeHeadUpdateEvent(String oldName, String newName, String projectName) {
+
+    this.oldName = oldName;
+    this.newName = newName;
+    this.projectName = projectName;
+  }
+
+  @Override
+  public NotifyHandling getNotify() {
+    return NotifyHandling.NONE;
+  }
+
+  @Override
+  public String getOldHeadName() {
+    return oldName;
+  }
+
+  @Override
+  public String getNewHeadName() {
+    return newName;
+  }
+
+  @Override
+  public String getProjectName() {
+    return projectName;
+  }
+}
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 9557025..c2335e8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.replication.pull;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
@@ -25,10 +26,25 @@
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -62,14 +78,33 @@
 
   @Inject private SitePaths sitePaths;
   @Inject private ProjectOperations projectOperations;
+  @Inject private DynamicSet<ProjectDeletedListener> deletedListeners;
   private Path gitPath;
   private FileBasedConfig config;
   private FileBasedConfig secureConfig;
 
+  static FakeDeleteProjectPlugin fakeDeleteProjectPlugin;
+
+  static class FakeDeleteModule extends AbstractModule {
+
+    @Override
+    public void configure() {
+      install(
+          new RestApiModule() {
+            @Override
+            public void configure() {
+              fakeDeleteProjectPlugin = new FakeDeleteProjectPlugin();
+              delete(PROJECT_KIND).toInstance(fakeDeleteProjectPlugin);
+            }
+          });
+    }
+  }
+
   @Override
   public void setUpTestPlugin() throws Exception {
     gitPath = sitePaths.site_path.resolve("git");
 
+    installPlugin("fakeDeleteProjectPlugin", FakeDeleteModule.class, null, null);
     config =
         new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
     setReplicationSource(
@@ -222,6 +257,67 @@
     }
   }
 
+  @Test
+  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(() -> fakeDeleteProjectPlugin.getDeleteEndpointCalls() == 1);
+  }
+
+  @Test
+  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);
+    String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision;
+
+    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;
+          }
+        });
+  }
+
   private Ref getRef(Repository repo, String branchName) throws IOException {
     return repo.getRefDatabase().exactRef(branchName);
   }
@@ -278,4 +374,24 @@
   private Project.NameKey createTestProject(String name) throws Exception {
     return projectOperations.newProject().name(name).create();
   }
+
+  @Singleton
+  public static class FakeDeleteProjectPlugin implements RestModifyView<ProjectResource, Input> {
+    private int deleteEndpointCalls;
+
+    FakeDeleteProjectPlugin() {
+      this.deleteEndpointCalls = 0;
+    }
+
+    @Override
+    public Response<?> apply(ProjectResource resource, Input input)
+        throws AuthException, BadRequestException, ResourceConflictException, Exception {
+      deleteEndpointCalls += 1;
+      return Response.ok();
+    }
+
+    int getDeleteEndpointCalls() {
+      return deleteEndpointCalls;
+    }
+  }
 }
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 286fe24..78eb3f7 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
@@ -14,20 +14,24 @@
 
 package com.googlesource.gerrit.plugins.replication.pull;
 
+import static com.google.common.truth.Truth.assertThat;
 import static java.nio.file.Files.createTempDirectory;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener.Event;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.EventDispatcher;
@@ -51,6 +55,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -71,6 +77,9 @@
   @Mock RevisionData revisionData;
   @Mock HttpResult httpResult;
 
+  @Captor ArgumentCaptor<String> stringCaptor;
+  @Captor ArgumentCaptor<Project.NameKey> projectNameKeyCaptor;
+
   private ExcludedRefsFilter refsFilter;
   private ReplicationQueue objectUnderTest;
   private SitePaths sitePaths;
@@ -261,6 +270,62 @@
     verifyZeroInteractions(wq, rd, dis, sl, fetchClientFactory, accountInfo);
   }
 
+  @Test
+  public void shouldCallDeleteWhenReplicateProjectDeletionsTrue() throws IOException {
+    when(source.wouldDeleteProject(any())).thenReturn(true);
+
+    String projectName = "testProject";
+    FakeProjectDeletedEvent event = new FakeProjectDeletedEvent(projectName);
+
+    objectUnderTest.start();
+    objectUnderTest.onProjectDeleted(event);
+
+    verify(source, times(1))
+        .scheduleDeleteProject(stringCaptor.capture(), projectNameKeyCaptor.capture());
+    assertThat(stringCaptor.getValue()).isEqualTo(source.getApis().get(0));
+    assertThat(projectNameKeyCaptor.getValue()).isEqualTo(Project.NameKey.parse(projectName));
+  }
+
+  @Test
+  public void shouldNotCallDeleteWhenProjectNotToDelete() throws IOException {
+    when(source.wouldDeleteProject(any())).thenReturn(false);
+
+    FakeProjectDeletedEvent event = new FakeProjectDeletedEvent("testProject");
+
+    objectUnderTest.start();
+    objectUnderTest.onProjectDeleted(event);
+
+    verify(source, never()).scheduleDeleteProject(any(), any());
+  }
+
+  @Test
+  public void shouldScheduleUpdateHeadWhenWouldFetchProject() throws IOException {
+    when(source.wouldFetchProject(any())).thenReturn(true);
+
+    String projectName = "aProject";
+    String newHEAD = "newHEAD";
+
+    objectUnderTest.start();
+    objectUnderTest.onHeadUpdated(new FakeHeadUpdateEvent("oldHead", newHEAD, projectName));
+    verify(source, times(1))
+        .scheduleUpdateHead(any(), projectNameKeyCaptor.capture(), stringCaptor.capture());
+
+    assertThat(stringCaptor.getValue()).isEqualTo(newHEAD);
+    assertThat(projectNameKeyCaptor.getValue()).isEqualTo(Project.NameKey.parse(projectName));
+  }
+
+  @Test
+  public void shouldNotScheduleUpdateHeadWhenNotWouldFetchProject() throws IOException {
+    when(source.wouldFetchProject(any())).thenReturn(false);
+
+    String projectName = "aProject";
+    String newHEAD = "newHEAD";
+
+    objectUnderTest.start();
+    objectUnderTest.onHeadUpdated(new FakeHeadUpdateEvent("oldHead", newHEAD, projectName));
+    verify(source, never()).scheduleUpdateHead(any(), any(), any());
+  }
+
   protected static Path createTempPath(String prefix) throws IOException {
     return createTempDirectory(prefix);
   }
@@ -325,4 +390,22 @@
       return null;
     }
   }
+
+  private class FakeProjectDeletedEvent implements ProjectDeletedListener.Event {
+    private String projectName;
+
+    public FakeProjectDeletedEvent(String projectName) {
+      this.projectName = projectName;
+    }
+
+    @Override
+    public NotifyHandling getNotify() {
+      return null;
+    }
+
+    @Override
+    public String getProjectName() {
+      return projectName;
+    }
+  }
 }
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 49cf608..343ce66 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -47,7 +48,9 @@
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.CredentialsProvider;
 import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.protocol.HttpClientContext;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.BasicCredentialsProvider;
@@ -79,7 +82,7 @@
   SourceHttpClient.Factory httpClientFactory;
   String url;
 
-  protected abstract String getURL();
+  protected abstract String getURL(String projectName);
 
   @Override
   public void setUpTestPlugin() throws Exception {
@@ -105,7 +108,7 @@
     revisionReader = plugin.getSysInjector().getInstance(RevisionReader.class);
     source = plugin.getSysInjector().getInstance(SourcesCollection.class).getAll().get(0);
 
-    url = getURL();
+    url = getURL(project.get());
   }
 
   protected HttpPost createRequest(String sendObjectPayload) {
@@ -115,6 +118,19 @@
     return post;
   }
 
+  protected HttpPut createPutRequest(String sendObjectPayload) {
+    HttpPut put = new HttpPut(url);
+    put.setEntity(new StringEntity(sendObjectPayload, StandardCharsets.UTF_8));
+    put.addHeader(new BasicHeader("Content-Type", "application/json"));
+    return put;
+  }
+
+  protected HttpDelete createDeleteRequest() {
+    HttpDelete delete = new HttpDelete(url);
+    delete.addHeader(new BasicHeader("Content-Type", "application/json"));
+    return delete;
+  }
+
   protected String createRef() throws Exception {
     return createRef(Project.nameKey(project + TEST_REPLICATION_SUFFIX));
   }
@@ -152,12 +168,11 @@
   }
 
   protected HttpClientContext getContext() {
-    HttpClientContext ctx = HttpClientContext.create();
-    CredentialsProvider adapted = new BasicCredentialsProvider();
-    adapted.setCredentials(
-        AuthScope.ANY, new UsernamePasswordCredentials(admin.username(), admin.httpPassword()));
-    ctx.setCredentialsProvider(adapted);
-    return ctx;
+    return getContextForAccount(admin);
+  }
+
+  protected HttpClientContext getUserContext() {
+    return getContextForAccount(user);
   }
 
   protected HttpClientContext getAnonymousContext() {
@@ -199,4 +214,13 @@
     secureConfig.setString("remote", remoteName, "password", password);
     secureConfig.save();
   }
+
+  private HttpClientContext getContextForAccount(TestAccount account) {
+    HttpClientContext ctx = HttpClientContext.create();
+    CredentialsProvider adapted = new BasicCredentialsProvider();
+    adapted.setCredentials(
+        AuthScope.ANY, new UsernamePasswordCredentials(account.username(), account.httpPassword()));
+    ctx.setCredentialsProvider(adapted);
+    return ctx;
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
index ec172ad..aad3903 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
@@ -176,9 +176,9 @@
   }
 
   @Override
-  protected String getURL() {
+  protected String getURL(String projectName) {
     return String.format(
         "%s/a/projects/%s/pull-replication~apply-object",
-        adminRestSession.url(), Url.encode(project.get()));
+        adminRestSession.url(), Url.encode(projectName));
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java
index 7b73df7..4ad8ce6 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java
@@ -76,9 +76,8 @@
   }
 
   @Override
-  protected String getURL() {
+  protected String getURL(String projectName) {
     return String.format(
-        "%s/a/projects/%s/pull-replication~fetch",
-        adminRestSession.url(), Url.encode(project.get()));
+        "%s/a/projects/%s/pull-replication~fetch", adminRestSession.url(), Url.encode(projectName));
   }
 }
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
new file mode 100644
index 0000000..4c09266
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
@@ -0,0 +1,101 @@
+// 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;
+
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+public class ProjectDeletionActionIT extends ActionITBase {
+  public static final String INVALID_TEST_PROJECT_NAME = "\0";
+  public static final String DELETE_PROJECT_PERMISSION = "delete-project-deleteProject";
+
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnUnauthorizedForUserWithoutPermissionsOnReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createDeleteRequest(),
+            assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED),
+            getAnonymousContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnOKWhenProjectIsDeletedOnReplica() throws Exception {
+    String testProjectName = project.get();
+    url = getURL(testProjectName);
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_OK), getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldDeleteRepositoryWhenUserHasProjectDeletionCapabilitiesAndNodeIsAReplica()
+      throws Exception {
+    String testProjectName = project.get();
+    url = getURL(testProjectName);
+    httpClientFactory
+        .create(source)
+        .execute(
+            createDeleteRequest(),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowCapability(DELETE_PROJECT_PERMISSION).group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createDeleteRequest(),
+            assertHttpResponseCode(HttpServletResponse.SC_OK),
+            getUserContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnInternalServerErrorIfProjectCannotBeDeletedWhenNodeIsAReplica()
+      throws Exception {
+    url = getURL(INVALID_TEST_PROJECT_NAME);
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createDeleteRequest(),
+            assertHttpResponseCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR),
+            getContext());
+  }
+
+  @Override
+  protected String getURL(String projectName) {
+    return String.format("%s/a/projects/%s", adminRestSession.url(), Url.encode(projectName));
+  }
+}
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 919ac34..ca4fa31 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
@@ -14,11 +14,16 @@
 
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction.getProjectInitializationUrl;
 
 import com.google.common.net.MediaType;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPut;
@@ -27,7 +32,7 @@
 
 public class ProjectInitializationActionIT extends ActionITBase {
   public static final String INVALID_TEST_PROJECT_NAME = "\0";
-  private String testProjectName = "new/Project";
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void shouldReturnUnauthorizedForUserWithoutPermissions() throws Exception {
@@ -51,6 +56,8 @@
 
   @Test
   public void shouldCreateRepository() throws Exception {
+    String newProjectName = "new/newProjectForPrimary";
+    url = getURL(newProjectName);
     httpClientFactory
         .create(source)
         .execute(
@@ -59,7 +66,7 @@
             getContext());
 
     HttpGet getNewProjectRequest =
-        new HttpGet(userRestSession.url() + "/a/projects/" + Url.encode("new/Project"));
+        new HttpGet(userRestSession.url() + "/a/projects/" + Url.encode(newProjectName));
     httpClientFactory
         .create(source)
         .execute(
@@ -67,8 +74,47 @@
   }
 
   @Test
+  public void shouldCreateRepositoryWhenUserHasProjectCreationCapabilities() throws Exception {
+    String newProjectName = "new/newProjectForUserWithCapabilities";
+    url = getURL(newProjectName);
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(
+            allowCapability(GlobalCapability.CREATE_PROJECT)
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_CREATED),
+            getUserContext());
+  }
+
+  @Test
+  public void shouldReturnForbiddenIfUserNotAuthorized() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+  }
+
+  @Test
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldCreateRepositoryWhenNodeIsAReplica() throws Exception {
+    String newProjectName = "new/newProjectForReplica";
+    url = getURL(newProjectName);
     httpClientFactory
         .create(source)
         .execute(
@@ -79,10 +125,49 @@
 
   @Test
   @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnForbiddenIfUserNotAuthorizedAndNodeIsAReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldCreateRepositoryWhenUserHasProjectCreationCapabilitiesAndNodeIsAReplica()
+      throws Exception {
+    String newProjectName = "new/newProjectForUserWithCapabilitiesReplica";
+    url = getURL(newProjectName);
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(
+            allowCapability(GlobalCapability.CREATE_PROJECT)
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_CREATED),
+            getUserContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
   public void shouldReturnInternalServerErrorIfProjectCannotBeCreatedWhenNodeIsAReplica()
       throws Exception {
-    testProjectName = INVALID_TEST_PROJECT_NAME;
-    url = getURL();
+    url = getURL(INVALID_TEST_PROJECT_NAME);
 
     httpClientFactory
         .create(source)
@@ -116,10 +201,10 @@
   }
 
   @Override
-  protected String getURL() {
+  protected String getURL(String projectName) {
     return userRestSession.url()
         + "/"
-        + getProjectInitializationUrl("pull-replication", Url.encode(testProjectName));
+        + getProjectInitializationUrl("pull-replication", Url.encode(projectName));
   }
 
   protected HttpPut createPutRequestWithHeaders() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java
new file mode 100644
index 0000000..aa07a7c
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java
@@ -0,0 +1,167 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+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.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.HeadInput;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+public class UpdateHeadActionIT extends ActionITBase {
+  private static final Gson gson = newGson();
+
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  public void shouldReturnUnauthorizedForUserWithoutPermissions() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput("some/branch")),
+            assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED),
+            getAnonymousContext());
+  }
+
+  @Test
+  public void shouldReturnBadRequestWhenInputIsEmpty() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput("")),
+            assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST),
+            getContext());
+  }
+
+  @Test
+  public void shouldReturnOKWhenHeadIsUpdated() throws Exception {
+    String testProjectName = project.get();
+    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);
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput(newBranch)),
+            assertHttpResponseCode(HttpServletResponse.SC_OK),
+            getContext());
+
+    assertThat(gApi.projects().name(testProjectName).head()).isEqualTo(newBranch);
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnBadRequestWhenInputIsEmptyInReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput("")),
+            assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST),
+            getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnOKWhenHeadIsUpdatedInReplica() throws Exception {
+    String testProjectName = project.get();
+    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);
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput(newBranch)),
+            assertHttpResponseCode(HttpServletResponse.SC_OK),
+            getContext());
+
+    assertThat(gApi.projects().name(testProjectName).head()).isEqualTo(newBranch);
+  }
+
+  @Test
+  public void shouldReturnForbiddenWhenMissingPermissions() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput("some/new/head")),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+  }
+
+  @Test
+  public void shouldReturnOKWhenRegisteredUserHasPermissions() throws Exception {
+    String testProjectName = project.get();
+    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);
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput(newBranch)),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput(newBranch)),
+            assertHttpResponseCode(HttpServletResponse.SC_OK),
+            getUserContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnForbiddenWhenMissingPermissionsInReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequest(headInput("some/new/head")),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+            getUserContext());
+  }
+
+  private String headInput(String ref) {
+    HeadInput headInput = new HeadInput();
+    headInput.ref = ref;
+    return gson.toJson(headInput);
+  }
+
+  @Override
+  protected String getURL(String projectName) {
+    return String.format("%s/a/projects/%s/HEAD", adminRestSession.url(), projectName);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
index 39a8f2d..ed1eef4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
@@ -23,7 +23,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.google.common.base.Charsets;
 import com.google.common.collect.Lists;
+import com.google.common.io.CharStreams;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
@@ -33,11 +35,13 @@
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
 import com.googlesource.gerrit.plugins.replication.pull.filter.SyncRefsFilter;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.net.URISyntaxException;
 import java.nio.ByteBuffer;
 import java.util.Optional;
 import org.apache.http.Header;
 import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
 import org.apache.http.message.BasicHeader;
@@ -69,6 +73,7 @@
   @Mock Source source;
   @Captor ArgumentCaptor<HttpPost> httpPostCaptor;
   @Captor ArgumentCaptor<HttpPut> httpPutCaptor;
+  @Captor ArgumentCaptor<HttpDelete> httpDeleteCaptor;
 
   String api = "http://gerrit-host";
   String pluginName = "pull-replication";
@@ -362,6 +367,37 @@
         .isEqualTo("/a/plugins/pull-replication/init-project/test_repo.git");
   }
 
+  @Test
+  public void shouldCallDeleteProjectEndpoint() throws IOException, URISyntaxException {
+
+    objectUnderTest.deleteProject(Project.nameKey("test_repo"), new URIish(api));
+
+    verify(httpClient, times(1)).execute(httpDeleteCaptor.capture(), any(), any());
+
+    HttpDelete httpDelete = httpDeleteCaptor.getValue();
+    assertThat(httpDelete.getURI().getHost()).isEqualTo("gerrit-host");
+    assertThat(httpDelete.getURI().getPath()).isEqualTo("/a/projects/test_repo");
+  }
+
+  @Test
+  public void shouldCallUpdateHEADEndpoint() throws IOException, URISyntaxException {
+    String newHead = "newHead";
+    String projectName = "aProject";
+    objectUnderTest.updateHead(Project.nameKey(projectName), newHead, new URIish(api));
+
+    verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any(), 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.getURI().getPath())
+        .isEqualTo(String.format("/a/projects/%s/HEAD", projectName));
+    assertThat(payload).isEqualTo(String.format("{\"ref\": \"%s\"}", newHead));
+  }
+
   public String readPayload(HttpPost entity) throws UnsupportedOperationException, IOException {
     ByteBuffer buf = IO.readWholeStream(entity.getEntity().getContent(), 1024);
     return RawParseUtils.decode(buf.array(), buf.arrayOffset(), buf.limit()).trim();