Merge branch 'stable-3.10' into stable-3.11

* stable-3.10:
  FetchOne: consider order of create/delete events when replicating deletions
  Consume only relevant stream events
  Use stream events to delete repositories

Change-Id: Ib3318cee50d8c43a3ff44bf5f50c2b73174a7c73
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 e75c4c3..5e7fda2 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
@@ -17,6 +17,7 @@
 import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -476,11 +477,7 @@
     try {
       List<FetchRefSpec> toFetch =
           fetchRefSpecs.stream().filter(rs -> rs.getSource() != null).toList();
-      Set<String> toDelete =
-          fetchRefSpecs.stream()
-              .filter(rs -> rs.getSource() == null)
-              .map(RefSpec::getDestination)
-              .collect(Collectors.toSet());
+      Set<String> toDelete = refsToDelete(fetchRefSpecs);
       updateStates(fetch.fetch(toFetch));
 
       // JGit doesn't support a fetch of <empty> to a ref (e.g. :refs/to/delete) therefore we have
@@ -512,6 +509,21 @@
     return fetchRefSpecs;
   }
 
+  @VisibleForTesting
+  static Set<String> refsToDelete(List<FetchRefSpec> fetchRefSpecs) {
+    final Set<String> refsToDelete = new HashSet<>();
+    fetchRefSpecs.forEach(
+        rs -> {
+          String refName = rs.refName();
+          if (rs.isDelete()) {
+            refsToDelete.add(refName);
+          } else {
+            refsToDelete.remove(refName);
+          }
+        });
+    return refsToDelete;
+  }
+
   /**
    * Return the list of refSpecs to fetch, possibly after having been filtered.
    *
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefSpec.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefSpec.java
index f85f22a..e41e194 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefSpec.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefSpec.java
@@ -44,6 +44,10 @@
     return MoreObjects.firstNonNull(getSource(), getDestination());
   }
 
+  public boolean isDelete() {
+    return getSource() == null;
+  }
+
   @Override
   public String toString() {
     return getSource() == null ? "<<DELETED>>:" + getDestination() : super.toString();
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 f9165ad..18574fd 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
@@ -29,6 +29,7 @@
 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.deleteproject.cache.CacheDeleteHandler;
 import com.googlesource.gerrit.plugins.deleteproject.fs.RepositoryDelete;
 import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
@@ -37,12 +38,13 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.transport.URIish;
 
-class ProjectDeletionAction
+@Singleton
+public class ProjectDeletionAction
     implements RestModifyView<ProjectResource, ProjectDeletionAction.DeleteInput> {
   private static final PluginPermission DELETE_PROJECT =
       new PluginPermission("delete-project", "deleteProject");
 
-  static class DeleteInput {}
+  public static class DeleteInput {}
 
   private final Provider<CurrentUser> userProvider;
   private final GerritConfigOps gerritConfigOps;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
index f863148..eb2d7e0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
@@ -25,7 +25,9 @@
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.config.GerritInstanceId;
 import com.google.gerrit.server.data.RefUpdateAttribute;
@@ -37,9 +39,12 @@
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.deleteproject.ProjectDeletedEvent;
 import com.googlesource.gerrit.plugins.replication.pull.ApplyObjectsCacheKey;
 import com.googlesource.gerrit.plugins.replication.pull.FetchOne;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
@@ -47,6 +52,7 @@
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory;
+import com.googlesource.gerrit.plugins.replication.pull.api.ProjectDeletionAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
 import com.googlesource.gerrit.plugins.replication.pull.api.UpdateHeadCommand;
@@ -68,6 +74,8 @@
   private final String instanceId;
   private final WorkQueue workQueue;
   private final Cache<ApplyObjectsCacheKey, Long> refUpdatesSucceededCache;
+  private final ProjectDeletionAction projectDeletionAction;
+  private final ProjectsCollection projectsCollection;
 
   @Inject
   public StreamEventListener(
@@ -79,7 +87,9 @@
       Provider<PullReplicationApiRequestMetrics> metricsProvider,
       SourcesCollection sources,
       ExcludedRefsFilter excludedRefsFilter,
-      @Named(APPLY_OBJECTS_CACHE) Cache<ApplyObjectsCacheKey, Long> refUpdatesSucceededCache) {
+      @Named(APPLY_OBJECTS_CACHE) Cache<ApplyObjectsCacheKey, Long> refUpdatesSucceededCache,
+      ProjectDeletionAction projectDeletionAction,
+      ProjectsCollection projectsCollection) {
     this.instanceId = instanceId;
     this.updateHeadCommand = updateHeadCommand;
     this.projectInitializationAction = projectInitializationAction;
@@ -89,6 +99,8 @@
     this.sources = sources;
     this.refsFilter = excludedRefsFilter;
     this.refUpdatesSucceededCache = refUpdatesSucceededCache;
+    this.projectDeletionAction = projectDeletionAction;
+    this.projectsCollection = projectsCollection;
 
     requireNonNull(
         Strings.emptyToNull(this.instanceId), "gerrit.instanceId cannot be null or empty");
@@ -173,6 +185,25 @@
             "Failed to update HEAD on project: %s", headUpdatedEvent.projectName);
         throw e;
       }
+    } else if (event instanceof ProjectDeletedEvent) {
+      deleteProject((ProjectDeletedEvent) event);
+    }
+  }
+
+  protected void deleteProject(ProjectEvent projectDeletedEvent) {
+    try {
+      ProjectResource projectResource =
+          projectsCollection.parse(
+              TopLevelResource.INSTANCE,
+              IdString.fromDecoded(projectDeletedEvent.getProjectNameKey().get()));
+      projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput());
+    } catch (ResourceNotFoundException e) {
+      logger.atFine().withCause(e).log(
+          "Repository not found whilst trying to delete project:%s",
+          projectDeletedEvent.getProjectNameKey().get());
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot delete project:%s", projectDeletedEvent.getProjectNameKey().get());
     }
   }
 
@@ -181,7 +212,7 @@
   }
 
   private boolean shouldReplicateProject(Event event) {
-    if (!(event instanceof ProjectEvent)) {
+    if (!isInterestingEventType(event)) {
       return false;
     }
 
@@ -202,10 +233,23 @@
           && source.wouldCreateProject(projectCreatedEvent.getProjectNameKey());
     }
 
+    if (event instanceof ProjectDeletedEvent) {
+      ProjectDeletedEvent projectDeletedEvent = (ProjectDeletedEvent) event;
+
+      return source.wouldDeleteProject(projectDeletedEvent.getProjectNameKey());
+    }
+
     ProjectEvent projectEvent = (ProjectEvent) event;
     return source.wouldFetchProject(projectEvent.getProjectNameKey());
   }
 
+  private static boolean isInterestingEventType(Event event) {
+    return event instanceof ProjectDeletedEvent
+        || event instanceof ProjectCreatedEvent
+        || event instanceof RefUpdatedEvent
+        || event instanceof ProjectHeadUpdatedEvent;
+  }
+
   private boolean isRefDelete(RefUpdatedEvent event) {
     return ZERO_ID_NAME.equals(event.refUpdate.get().newRev);
   }
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 49e8d6f..73715d6 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
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.replication.pull;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.replication.pull.FetchOne.refsToDelete;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.anyList;
 import static org.mockito.Mockito.argThat;
@@ -850,6 +851,36 @@
     assertThat(testMetricMaker.getTimer("replication_latency")).isGreaterThan(0);
   }
 
+  @Test
+  public void shouldSkipDeletionWhenDeleteAndCreateOfSameRef() {
+    List<FetchRefSpec> fetchRefSpecs =
+        List.of(
+            FetchRefSpec.fromRef(":refs/something/someref"),
+            FetchRefSpec.fromRef("refs/something/someref"));
+    assertThat(refsToDelete(fetchRefSpecs)).isEmpty();
+  }
+
+  @Test
+  public void shouldDeleteWhenCreateAndDeleteOfSameRef() {
+    List<FetchRefSpec> fetchRefSpecs =
+        List.of(
+            FetchRefSpec.fromRef("refs/something/someref"),
+            FetchRefSpec.fromRef(":refs/something/someref"));
+    assertThat(refsToDelete(fetchRefSpecs)).isEqualTo(Set.of("refs/something/someref"));
+  }
+
+  @Test
+  public void shouldNotDeleteWhenCreateRef() {
+    List<FetchRefSpec> fetchRefSpecs = List.of(FetchRefSpec.fromRef("refs/something/someref"));
+    assertThat(refsToDelete(fetchRefSpecs)).isEmpty();
+  }
+
+  @Test
+  public void shouldDeleteWhenDeleteRef() {
+    List<FetchRefSpec> fetchRefSpecs = List.of(FetchRefSpec.fromRef(":refs/something/someref"));
+    assertThat(refsToDelete(fetchRefSpecs)).isEqualTo(Set.of("refs/something/someref"));
+  }
+
   private void setupRequestScopeMock() {
     when(scoper.scope(any()))
         .thenAnswer(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
index a60fed6..980c509 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
@@ -31,12 +31,14 @@
 import com.google.gerrit.server.events.ProjectHeadUpdatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.googlesource.gerrit.plugins.replication.pull.ApplyObjectsCacheKey;
 import com.googlesource.gerrit.plugins.replication.pull.FetchOne;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
 import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
+import com.googlesource.gerrit.plugins.replication.pull.api.ProjectDeletionAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
 import com.googlesource.gerrit.plugins.replication.pull.api.UpdateHeadCommand;
@@ -72,6 +74,9 @@
   @Mock private SourcesCollection sources;
   @Mock private Source source;
   @Mock private ExcludedRefsFilter refsFilter;
+  @Mock private ProjectDeletionAction projectDeletionAction;
+  @Mock private ProjectsCollection projectsCollection;
+
   private Cache<ApplyObjectsCacheKey, Long> cache;
 
   private StreamEventListener objectUnderTest;
@@ -98,7 +103,9 @@
             () -> metrics,
             sources,
             refsFilter,
-            cache);
+            cache,
+            projectDeletionAction,
+            projectsCollection);
   }
 
   @Test