Manage change deletion performed by pull-replication

When a delete change is replicated through the pull-replication plugin,
the fetch-ref-replicated event contains the ref-name of the change /meta
with a FORCED|NO_CHANGE result and an inexistent ref on the repository.

Leverage the event in the FetchRefReplicatedEventHandler and delete also
the change from the index.

Depends-On: https://gerrit-review.googlesource.com/c/plugins/replication/+/480184?
Change-Id: I8567a583fc149eb630c72eb45084a5e7b423decb
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java
index d45f526..83076bf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java
@@ -21,21 +21,30 @@
 import com.google.gerrit.server.config.GerritInstanceId;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
+import java.io.IOException;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
 
 public class FetchRefReplicatedEventHandler implements EventListener {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final GitRepositoryManager gitRepositoryManager;
   private ChangeIndexer changeIndexer;
   private final String instanceId;
 
   @Inject
-  FetchRefReplicatedEventHandler(ChangeIndexer changeIndexer, @GerritInstanceId String instanceId) {
+  FetchRefReplicatedEventHandler(
+      ChangeIndexer changeIndexer,
+      @GerritInstanceId String instanceId,
+      GitRepositoryManager gitRepositoryManager) {
     this.changeIndexer = changeIndexer;
     this.instanceId = instanceId;
+    this.gitRepositoryManager = gitRepositoryManager;
   }
 
   @Override
@@ -46,7 +55,15 @@
 
       if (event instanceof FetchRefReplicatedEvent && isLocalEvent(event)) {
         FetchRefReplicatedEvent fetchRefReplicatedEvent = (FetchRefReplicatedEvent) event;
-        if (!RefNames.isNoteDbMetaRef(fetchRefReplicatedEvent.getRefName())
+        // This removal of the ':' prefix is needed because of Issue 426470297 in the
+        // pull-replication plugin
+        // that may leave the deleted refs notification as a ref-spec instead of the deleted
+        // ref-name.
+        String refName =
+            fetchRefReplicatedEvent.ref.startsWith(":")
+                ? fetchRefReplicatedEvent.ref.substring(1)
+                : fetchRefReplicatedEvent.ref;
+        if (!RefNames.isNoteDbMetaRef(refName)
             || !fetchRefReplicatedEvent
                 .getStatus()
                 .equals(ReplicationState.RefFetchResult.SUCCEEDED.toString())) {
@@ -54,18 +71,44 @@
         }
 
         Project.NameKey projectNameKey = fetchRefReplicatedEvent.getProjectNameKey();
-        logger.atFine().log(
-            "Indexing ref '%s' for project %s",
-            fetchRefReplicatedEvent.getRefName(), projectNameKey.get());
-        Change.Id changeId = Change.Id.fromRef(fetchRefReplicatedEvent.getRefName());
-        if (changeId != null) {
-          changeIndexer.index(projectNameKey, changeId);
-        } else {
-          logger.atWarning().log(
-              "Couldn't get changeId from refName. Skipping indexing of change %s for project %s",
-              fetchRefReplicatedEvent.getRefName(), projectNameKey.get());
+        Change.Id changeId = Change.Id.fromRef(refName);
+
+        try (Repository repo = gitRepositoryManager.openRepository(projectNameKey)) {
+          if (changeId != null) {
+            RefUpdate.Result fetchRefReplicatedEventResult =
+                fetchRefReplicatedEvent.getRefUpdateResult();
+            if ((RefUpdate.Result.FORCED.equals(fetchRefReplicatedEventResult)
+                    || RefUpdate.Result.NO_CHANGE.equals(fetchRefReplicatedEventResult))
+                && repo.exactRef(refName) == null) {
+              logger.atInfo().log(
+                  "Deleting change '%s' from index following the forced delete of '%s' on project"
+                      + " %s",
+                  changeId, refName, projectNameKey.get());
+              // NOTE: The change has been already deleted and we just don't know if it was a
+              // locally
+              // created change or imported, because the meta-data is not there anymore.
+              // In the future, it would be best to have that information passed to the event by the
+              // pull-replication plugin *before* the delete, and added to the
+              // FetchRefReplicatedEvent,
+              // so that multi-site can use it for computing the virtual change-id and assuring the
+              // delete
+              // of the imported changes as well.
+              changeIndexer.delete(changeId);
+            } else {
+              logger.atInfo().log(
+                  "Indexing change '%s' following update of '%s' on project %s",
+                  changeId, refName, projectNameKey.get());
+              changeIndexer.index(projectNameKey, changeId);
+            }
+          } else {
+            logger.atWarning().log(
+                "Couldn't get changeId from refName. Skipping indexing of change %s for project %s",
+                refName, projectNameKey.get());
+          }
         }
       }
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log("Unable to process event %s", event);
     } finally {
       Context.setForwardedEvent(isForwarded);
     }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java
index 47b6072..ed2f958 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java
@@ -16,6 +16,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -23,12 +24,14 @@
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.googlesource.gerrit.plugins.replication.pull.Context;
 import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState.RefFetchResult;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.URIish;
 import org.junit.Before;
 import org.junit.Test;
@@ -36,20 +39,46 @@
 public class FetchRefReplicatedEventHandlerTest {
   private static final String LOCAL_INSTANCE_ID = "local-instance-id";
   private static final String REMOTE_INSTANCE_ID = "remote-instance-id";
+  private static final Project.NameKey PROJECT_NAME_KEY = Project.nameKey("testProject");
   private ChangeIndexer changeIndexerMock;
+  private GitRepositoryManager gitRepositoryManager;
+  private Repository repository;
   private FetchRefReplicatedEventHandler fetchRefReplicatedEventHandler;
   private static URIish sourceUri;
 
   @Before
   public void setUp() throws Exception {
     changeIndexerMock = mock(ChangeIndexer.class);
+    gitRepositoryManager = mock(GitRepositoryManager.class);
+    repository = mock(Repository.class);
+    doReturn(repository).when(gitRepositoryManager).openRepository(any());
     fetchRefReplicatedEventHandler =
-        new FetchRefReplicatedEventHandler(changeIndexerMock, LOCAL_INSTANCE_ID);
+        new FetchRefReplicatedEventHandler(
+            changeIndexerMock, LOCAL_INSTANCE_ID, gitRepositoryManager);
     sourceUri = new URIish("git://aSourceNode/testProject.git");
   }
 
   @Test
   public void onEventShouldIndexExistingChange() {
+    String ref = "refs/changes/41/41/meta";
+    Change.Id changeId = Change.Id.fromRef(ref);
+    try {
+      Context.setLocalEvent(true);
+      fetchRefReplicatedEventHandler.onEvent(
+          newFetchRefReplicatedEvent(
+              PROJECT_NAME_KEY,
+              ref,
+              ReplicationState.RefFetchResult.SUCCEEDED,
+              LOCAL_INSTANCE_ID,
+              RefUpdate.Result.FAST_FORWARD));
+      verify(changeIndexerMock, times(1)).index(eq(PROJECT_NAME_KEY), eq(changeId));
+    } finally {
+      Context.unsetLocalEvent();
+    }
+  }
+
+  @Test
+  public void onEventShouldDeleteChangeFromIndex() {
     Project.NameKey projectNameKey = Project.nameKey("testProject");
     String ref = "refs/changes/41/41/meta";
     Change.Id changeId = Change.Id.fromRef(ref);
@@ -57,8 +86,12 @@
       Context.setLocalEvent(true);
       fetchRefReplicatedEventHandler.onEvent(
           newFetchRefReplicatedEvent(
-              projectNameKey, ref, ReplicationState.RefFetchResult.SUCCEEDED, LOCAL_INSTANCE_ID));
-      verify(changeIndexerMock, times(1)).index(eq(projectNameKey), eq(changeId));
+              projectNameKey,
+              ref,
+              ReplicationState.RefFetchResult.SUCCEEDED,
+              LOCAL_INSTANCE_ID,
+              RefUpdate.Result.FORCED));
+      verify(changeIndexerMock, times(1)).delete(eq(changeId));
     } finally {
       Context.unsetLocalEvent();
     }
@@ -71,7 +104,11 @@
     Change.Id changeId = Change.Id.fromRef(ref);
     fetchRefReplicatedEventHandler.onEvent(
         newFetchRefReplicatedEvent(
-            projectNameKey, ref, ReplicationState.RefFetchResult.SUCCEEDED, REMOTE_INSTANCE_ID));
+            projectNameKey,
+            ref,
+            ReplicationState.RefFetchResult.SUCCEEDED,
+            REMOTE_INSTANCE_ID,
+            RefUpdate.Result.FAST_FORWARD));
     verify(changeIndexerMock, never()).index(eq(projectNameKey), eq(changeId));
   }
 
@@ -82,7 +119,11 @@
     Change.Id changeId = Change.Id.fromRef(ref);
     fetchRefReplicatedEventHandler.onEvent(
         newFetchRefReplicatedEvent(
-            projectNameKey, ref, ReplicationState.RefFetchResult.SUCCEEDED, LOCAL_INSTANCE_ID));
+            projectNameKey,
+            ref,
+            ReplicationState.RefFetchResult.SUCCEEDED,
+            LOCAL_INSTANCE_ID,
+            RefUpdate.Result.FAST_FORWARD));
     verify(changeIndexerMock, never()).index(eq(projectNameKey), eq(changeId));
   }
 
@@ -93,7 +134,8 @@
             Project.nameKey("testProject"),
             "invalidRef",
             ReplicationState.RefFetchResult.SUCCEEDED,
-            LOCAL_INSTANCE_ID));
+            LOCAL_INSTANCE_ID,
+            RefUpdate.Result.FAST_FORWARD));
     verify(changeIndexerMock, never()).index(any(), any());
   }
 
@@ -103,7 +145,11 @@
     String ref = "refs/changes/41/41/meta";
     fetchRefReplicatedEventHandler.onEvent(
         newFetchRefReplicatedEvent(
-            projectNameKey, ref, ReplicationState.RefFetchResult.FAILED, LOCAL_INSTANCE_ID));
+            projectNameKey,
+            ref,
+            ReplicationState.RefFetchResult.FAILED,
+            LOCAL_INSTANCE_ID,
+            RefUpdate.Result.FAST_FORWARD));
     verify(changeIndexerMock, never()).index(any(), any());
   }
 
@@ -113,15 +159,23 @@
     String ref = "refs/changes/41/41/meta";
     fetchRefReplicatedEventHandler.onEvent(
         newFetchRefReplicatedEvent(
-            projectNameKey, ref, ReplicationState.RefFetchResult.NOT_ATTEMPTED, LOCAL_INSTANCE_ID));
+            projectNameKey,
+            ref,
+            ReplicationState.RefFetchResult.NOT_ATTEMPTED,
+            LOCAL_INSTANCE_ID,
+            RefUpdate.Result.FAST_FORWARD));
     verify(changeIndexerMock, never()).index(any(), any());
   }
 
   private FetchRefReplicatedEvent newFetchRefReplicatedEvent(
-      Project.NameKey projectNameKey, String ref, RefFetchResult fetchResult, String instanceId) {
+      Project.NameKey projectNameKey,
+      String ref,
+      RefFetchResult fetchResult,
+      String instanceId,
+      RefUpdate.Result refUpdateResult) {
     FetchRefReplicatedEvent event =
         new FetchRefReplicatedEvent(
-            projectNameKey.get(), ref, sourceUri, fetchResult, RefUpdate.Result.FAST_FORWARD);
+            projectNameKey.get(), ref, sourceUri, fetchResult, refUpdateResult);
     event.instanceId = instanceId;
     return event;
   }