Merge "Listen to GitBatchRefUpdateListener for a set of events" into stable-3.6
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 cd19be4..08a402e 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
@@ -19,7 +19,7 @@
 import com.google.common.eventbus.EventBus;
 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.GitBatchRefUpdateListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -123,7 +123,7 @@
         .annotatedWith(UniqueAnnotations.create())
         .to(ReplicationQueue.class);
 
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReplicationQueue.class);
+    DynamicSet.bind(binder(), GitBatchRefUpdateListener.class).to(ReplicationQueue.class);
 
     bind(ConfigParser.class).to(SourceConfigParser.class).in(Scopes.SINGLETON);
 
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 f5b27ee..990c82c 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
@@ -20,7 +20,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -65,7 +65,7 @@
 public class ReplicationQueue
     implements ObservableQueue,
         LifecycleListener,
-        GitReferenceUpdatedListener,
+        GitBatchRefUpdateListener,
         ProjectDeletedListener,
         HeadUpdatedListener {
 
@@ -147,17 +147,24 @@
   }
 
   @Override
-  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    if (isRefToBeReplicated(event.getRefName())) {
-      repLog.info(
-          "Ref event received: {} on project {}:{} - {} => {}",
-          refUpdateType(event),
-          event.getProjectName(),
-          event.getRefName(),
-          event.getOldObjectId(),
-          event.getNewObjectId());
-      fire(ReferenceUpdatedEvent.from(event));
-    }
+  public void onGitBatchRefUpdate(GitBatchRefUpdateListener.Event event) {
+    event.getUpdatedRefs().stream()
+        .sorted(ReplicationQueue::sortByMetaRefAsLast)
+        .forEachOrdered(
+            updateRef -> {
+              String refName = updateRef.getRefName();
+
+              if (isRefToBeReplicated(refName)) {
+                repLog.info(
+                    "Ref event received: {} on project {}:{} - {} => {}",
+                    refUpdateType(updateRef),
+                    event.getProjectName(),
+                    refName,
+                    updateRef.getOldObjectId(),
+                    updateRef.getNewObjectId());
+                fire(ReferenceUpdatedEvent.from(event.getProjectName(), updateRef));
+              }
+            });
   }
 
   @Override
@@ -170,11 +177,17 @@
                 source.getApis().forEach(apiUrl -> source.scheduleDeleteProject(apiUrl, project)));
   }
 
-  private static String refUpdateType(GitReferenceUpdatedListener.Event event) {
-    String forcedPrefix = event.isNonFastForward() ? "FORCED " : " ";
-    if (event.isCreate()) {
+  private static int sortByMetaRefAsLast(UpdatedRef a, @SuppressWarnings("unused") UpdatedRef b) {
+    repLog.info("sortByMetaRefAsLast(" + a.getRefName() + " <=> " + b.getRefName());
+    return Boolean.compare(
+        RefNames.isNoteDbMetaRef(a.getRefName()), RefNames.isNoteDbMetaRef(b.getRefName()));
+  }
+
+  private static String refUpdateType(UpdatedRef updateRef) {
+    String forcedPrefix = updateRef.isNonFastForward() ? "FORCED " : " ";
+    if (updateRef.isCreate()) {
       return forcedPrefix + "CREATE";
-    } else if (event.isDelete()) {
+    } else if (updateRef.isDelete()) {
       return forcedPrefix + "DELETE";
     } else {
       return forcedPrefix + "UPDATE";
@@ -518,12 +531,12 @@
           projectName, refName, objectId, isDelete);
     }
 
-    static ReferenceUpdatedEvent from(GitReferenceUpdatedListener.Event event) {
+    static ReferenceUpdatedEvent from(String projectName, UpdatedRef updateRef) {
       return ReferenceUpdatedEvent.create(
-          event.getProjectName(),
-          event.getRefName(),
-          ObjectId.fromString(event.getNewObjectId()),
-          event.isDelete());
+          projectName,
+          updateRef.getRefName(),
+          ObjectId.fromString(updateRef.getNewObjectId()),
+          updateRef.isDelete());
     }
 
     public abstract String projectName();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FakeGitReferenceUpdatedEvent.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FakeGitReferenceUpdatedEvent.java
index 43331dc..69549aa 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FakeGitReferenceUpdatedEvent.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FakeGitReferenceUpdatedEvent.java
@@ -17,10 +17,13 @@
 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.GitBatchRefUpdateListener;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener.UpdatedRef;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
-public class FakeGitReferenceUpdatedEvent implements GitReferenceUpdatedListener.Event {
+public class FakeGitReferenceUpdatedEvent implements GitBatchRefUpdateListener.Event {
   private final String projectName;
   private final String ref;
   private final String oldObjectId;
@@ -46,36 +49,6 @@
   }
 
   @Override
-  public String getRefName() {
-    return ref;
-  }
-
-  @Override
-  public String getOldObjectId() {
-    return oldObjectId;
-  }
-
-  @Override
-  public String getNewObjectId() {
-    return newObjectId;
-  }
-
-  @Override
-  public boolean isCreate() {
-    return type == ReceiveCommand.Type.CREATE;
-  }
-
-  @Override
-  public boolean isDelete() {
-    return type == ReceiveCommand.Type.DELETE;
-  }
-
-  @Override
-  public boolean isNonFastForward() {
-    return type == ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
-  }
-
-  @Override
   public AccountInfo getUpdater() {
     return null;
   }
@@ -91,4 +64,46 @@
   public NotifyHandling getNotify() {
     return NotifyHandling.ALL;
   }
+
+  @Override
+  public Set<UpdatedRef> getUpdatedRefs() {
+    return Set.of(
+        new GitBatchRefUpdateListener.UpdatedRef() {
+
+          @Override
+          public String getRefName() {
+            return ref;
+          }
+
+          @Override
+          public String getOldObjectId() {
+            return ObjectId.zeroId().getName();
+          }
+
+          @Override
+          public String getNewObjectId() {
+            return newObjectId;
+          }
+
+          @Override
+          public boolean isCreate() {
+            return type == ReceiveCommand.Type.CREATE;
+          }
+
+          @Override
+          public boolean isDelete() {
+            return type == ReceiveCommand.Type.DELETE;
+          }
+
+          @Override
+          public boolean isNonFastForward() {
+            return type == ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
+          }
+        });
+  }
+
+  @Override
+  public Set<String> getRefNames() {
+    return Set.of(ref);
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
index ee5876f..ff8265f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
@@ -105,14 +105,14 @@
     String sourceRef = pushResult.getPatchSet().refName();
 
     ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             sourceRef,
             ObjectId.zeroId().getName(),
             sourceCommit.getId().getName(),
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project)) {
       waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
@@ -141,14 +141,14 @@
     RevCommit sourceCommit = pushResult.getCommit();
     final String sourceRef = pushResult.getPatchSet().refName();
     ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             sourceRef,
             ObjectId.zeroId().getName(),
             sourceCommit.getId().getName(),
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project)) {
       waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
@@ -174,14 +174,14 @@
 
     ReplicationQueue pullReplicationQueue =
         plugin.getSysInjector().getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             newBranch,
             ObjectId.zeroId().getName(),
             branchRevision,
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project);
         Repository sourceRepo = repoManager.openRepository(project)) {
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 86b22b2..6fd5cb3 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
@@ -30,7 +30,7 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -91,14 +91,14 @@
     String sourceRef = pushResult.getPatchSet().refName();
 
     ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             sourceRef,
             ObjectId.zeroId().getName(),
             sourceCommit.getId().getName(),
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project)) {
       waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
@@ -124,14 +124,14 @@
 
     ReplicationQueue pullReplicationQueue =
         plugin.getSysInjector().getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             newBranch,
             ObjectId.zeroId().getName(),
             branchRevision,
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project);
         Repository sourceRepo = repoManager.openRepository(project)) {
@@ -167,14 +167,14 @@
 
     ReplicationQueue pullReplicationQueue =
         plugin.getSysInjector().getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             newBranch,
             ObjectId.zeroId().getName(),
             branchRevision,
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project)) {
       waitUntil(() -> checkedGetRef(repo, newBranch) != null);
@@ -193,14 +193,14 @@
     assertThat(pushedRefs).hasSize(1);
     assertThat(pushedRefs.iterator().next().getStatus()).isEqualTo(Status.OK);
 
-    GitReferenceUpdatedListener.Event forcedPushEvent =
+    GitBatchRefUpdateListener.Event forcedPushEvent =
         new FakeGitReferenceUpdatedEvent(
             project,
             newBranch,
             branchRevision,
             amendedCommit.getId().getName(),
             ReceiveCommand.Type.UPDATE_NONFASTFORWARD);
-    pullReplicationQueue.onGitReferenceUpdated(forcedPushEvent);
+    pullReplicationQueue.onGitBatchRefUpdate(forcedPushEvent);
 
     try (Repository repo = repoManager.openRepository(project);
         Repository sourceRepo = repoManager.openRepository(project)) {
@@ -232,14 +232,14 @@
     String sourceRef = pushResult.getPatchSet().refName();
 
     ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             sourceRef,
             ObjectId.zeroId().getName(),
             sourceCommit.getId().getName(),
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project)) {
       waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
@@ -273,14 +273,14 @@
 
     ReplicationQueue pullReplicationQueue =
         plugin.getSysInjector().getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             newBranch,
             ObjectId.zeroId().getName(),
             branchRevision,
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project);
         Repository sourceRepo = repoManager.openRepository(project)) {
@@ -364,14 +364,14 @@
     String sourceRef = pushResult.getPatchSet().refName();
 
     ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             sourceRef,
             ObjectId.zeroId().getName(),
             sourceCommit.getId().getName(),
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project)) {
       waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
index d8e5947..2d1f51f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
@@ -82,14 +82,14 @@
     String sourceRef = pushResult.getPatchSet().refName();
 
     ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
-    GitReferenceUpdatedListener.Event event =
+    GitBatchRefUpdateListener.Event event =
         new FakeGitReferenceUpdatedEvent(
             project,
             sourceRef,
             ObjectId.zeroId().getName(),
             sourceCommit.getId().getName(),
             ReceiveCommand.Type.CREATE);
-    pullReplicationQueue.onGitReferenceUpdated(event);
+    pullReplicationQueue.onGitBatchRefUpdate(event);
 
     try (Repository repo = repoManager.openRepository(project)) {
       waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
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 927aec2..e7de264 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
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -33,8 +34,8 @@
 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.GitBatchRefUpdateListener;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener.UpdatedRef;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.metrics.DisabledMetricMaker;
@@ -54,6 +55,8 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.apache.http.client.ClientProtocolException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -64,6 +67,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -161,57 +165,84 @@
 
   @Test
   public void shouldCallSendObjectWhenMetaRef() throws ClientProtocolException, IOException {
-    Event event = new TestEvent("refs/changes/01/1/meta");
+    TestEvent event = new TestEvent("refs/changes/01/1/meta");
     objectUnderTest.start();
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient).callSendObjects(any(), anyString(), any(), any());
   }
 
   @Test
   public void shouldCallInitProjectWhenProjectIsMissing() throws IOException {
-    Event event = new TestEvent("refs/changes/01/1/meta");
+    TestEvent event = new TestEvent("refs/changes/01/1/meta");
     when(httpResult.isSuccessful()).thenReturn(false);
     when(httpResult.isProjectMissing(any())).thenReturn(true);
     when(source.isCreateMissingRepositories()).thenReturn(true);
 
     objectUnderTest.start();
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient).initProject(any(), any());
   }
 
   @Test
   public void shouldNotCallInitProjectWhenReplicateNewRepositoriesNotSet() throws IOException {
-    Event event = new TestEvent("refs/changes/01/1/meta");
+    TestEvent event = new TestEvent("refs/changes/01/1/meta");
     when(httpResult.isSuccessful()).thenReturn(false);
     when(httpResult.isProjectMissing(any())).thenReturn(true);
     when(source.isCreateMissingRepositories()).thenReturn(false);
 
     objectUnderTest.start();
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient, never()).initProject(any(), any());
   }
 
   @Test
   public void shouldCallSendObjectWhenPatchSetRef() throws ClientProtocolException, IOException {
-    Event event = new TestEvent("refs/changes/01/1/1");
+    TestEvent event = new TestEvent("refs/changes/01/1/1");
     objectUnderTest.start();
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient).callSendObjects(any(), anyString(), any(), any());
   }
 
   @Test
+  public void shouldCallSendObjectReorderingRefsHavingMetaAtTheEnd()
+      throws ClientProtocolException, IOException {
+    sendRefUpdatedEvents("refs/changes/01/1/meta", "refs/changes/01/1/1");
+    verifySendObjectOrdering("refs/changes/01/1/1", "refs/changes/01/1/meta");
+  }
+
+  @Test
+  public void shouldCallSendObjectKeepingMetaAtTheEnd()
+      throws ClientProtocolException, IOException {
+    sendRefUpdatedEvents("refs/changes/01/1/1", "refs/changes/01/1/meta");
+    verifySendObjectOrdering("refs/changes/01/1/1", "refs/changes/01/1/meta");
+  }
+
+  private void sendRefUpdatedEvents(String firstRef, String secondRef) {
+    objectUnderTest.start();
+    objectUnderTest.onGitBatchRefUpdate(new TestEvent(firstRef, secondRef));
+  }
+
+  private void verifySendObjectOrdering(String firstRef, String secondRef)
+      throws ClientProtocolException, IOException {
+    InOrder inOrder = inOrder(fetchRestApiClient);
+
+    inOrder.verify(fetchRestApiClient).callSendObjects(any(), eq(firstRef), any(), any());
+    inOrder.verify(fetchRestApiClient).callSendObjects(any(), eq(secondRef), any(), any());
+  }
+
+  @Test
   public void shouldFallbackToCallFetchWhenIOException()
       throws ClientProtocolException, IOException, LargeObjectException {
-    Event event = new TestEvent("refs/changes/01/1/meta");
+    TestEvent event = new TestEvent("refs/changes/01/1/meta");
     objectUnderTest.start();
 
     when(revReader.read(any(), any(), anyString(), anyInt())).thenThrow(IOException.class);
 
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient).callFetch(any(), anyString(), any());
   }
@@ -219,12 +250,12 @@
   @Test
   public void shouldFallbackToCallFetchWhenLargeRef()
       throws ClientProtocolException, IOException, LargeObjectException {
-    Event event = new TestEvent("refs/changes/01/1/1");
+    TestEvent event = new TestEvent("refs/changes/01/1/1");
     objectUnderTest.start();
 
     when(revReader.read(any(), any(), anyString(), anyInt())).thenReturn(Optional.empty());
 
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient).callFetch(any(), anyString(), any());
   }
@@ -232,7 +263,7 @@
   @Test
   public void shouldFallbackToCallFetchWhenParentObjectIsMissing()
       throws ClientProtocolException, IOException {
-    Event event = new TestEvent("refs/changes/01/1/1");
+    TestEvent event = new TestEvent("refs/changes/01/1/1");
     objectUnderTest.start();
 
     when(httpResult.isSuccessful()).thenReturn(false);
@@ -240,7 +271,7 @@
     when(fetchRestApiClient.callSendObjects(any(), anyString(), any(), any()))
         .thenReturn(httpResult);
 
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient).callFetch(any(), anyString(), any());
   }
@@ -248,7 +279,7 @@
   @Test
   public void shouldFallbackToApplyAllParentObjectsWhenParentObjectIsMissingOnMetaRef()
       throws ClientProtocolException, IOException {
-    Event event = new TestEvent("refs/changes/01/1/meta");
+    TestEvent event = new TestEvent("refs/changes/01/1/meta");
     objectUnderTest.start();
 
     when(httpResult.isSuccessful()).thenReturn(false, true);
@@ -256,7 +287,7 @@
     when(fetchRestApiClient.callSendObjects(any(), anyString(), any(), any()))
         .thenReturn(httpResult);
 
-    objectUnderTest.onGitReferenceUpdated(event);
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verify(fetchRestApiClient, times(2))
         .callSendObjects(any(), anyString(), revisionsDataCaptor.capture(), any());
@@ -291,16 +322,16 @@
             () -> revReader,
             applyObjectMetrics,
             fetchMetrics);
-    Event event = new TestEvent("refs/multi-site/version");
-    objectUnderTest.onGitReferenceUpdated(event);
+    TestEvent event = new TestEvent("refs/multi-site/version");
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verifyZeroInteractions(wq, rd, dis, sl, fetchClientFactory, accountInfo);
   }
 
   @Test
   public void shouldSkipEventWhenStarredChangesRef() {
-    Event event = new TestEvent("refs/starred-changes/41/2941/1000000");
-    objectUnderTest.onGitReferenceUpdated(event);
+    TestEvent event = new TestEvent("refs/starred-changes/41/2941/1000000");
+    objectUnderTest.onGitBatchRefUpdate(event);
 
     verifyZeroInteractions(wq, rd, dis, sl, fetchClientFactory, accountInfo);
   }
@@ -365,24 +396,22 @@
     return createTempDirectory(prefix);
   }
 
-  private class TestEvent implements GitReferenceUpdatedListener.Event {
+  private static class TestEvent implements GitBatchRefUpdateListener.Event {
     private String refName;
     private String projectName;
-    private ObjectId newObjectId;
+    private List<UpdatedRef> refs;
 
-    public TestEvent(String refName) {
-      this(refName, "defaultProject", ObjectId.zeroId());
+    public TestEvent(String... refNames) {
+      this(
+          "defaultProject",
+          Arrays.stream(refNames)
+              .map(refName -> updateRef(refName, ObjectId.zeroId()))
+              .collect(Collectors.toUnmodifiableList()));
     }
 
-    public TestEvent(String refName, String projectName, ObjectId newObjectId) {
-      this.refName = refName;
+    public TestEvent(String projectName, List<UpdatedRef> refs) {
       this.projectName = projectName;
-      this.newObjectId = newObjectId;
-    }
-
-    @Override
-    public String getRefName() {
-      return refName;
+      this.refs = refs;
     }
 
     @Override
@@ -396,34 +425,55 @@
     }
 
     @Override
-    public String getOldObjectId() {
-      return ObjectId.zeroId().getName();
-    }
-
-    @Override
-    public String getNewObjectId() {
-      return newObjectId.getName();
-    }
-
-    @Override
-    public boolean isCreate() {
-      return false;
-    }
-
-    @Override
-    public boolean isDelete() {
-      return false;
-    }
-
-    @Override
-    public boolean isNonFastForward() {
-      return false;
-    }
-
-    @Override
     public AccountInfo getUpdater() {
       return null;
     }
+
+    @Override
+    public Set<UpdatedRef> getUpdatedRefs() {
+      return refs.stream().collect(Collectors.toSet());
+    }
+
+    private static final GitBatchRefUpdateListener.UpdatedRef updateRef(
+        String refName, ObjectId refObjectId) {
+      return new GitBatchRefUpdateListener.UpdatedRef() {
+
+        @Override
+        public String getRefName() {
+          return refName;
+        }
+
+        @Override
+        public String getOldObjectId() {
+          return ObjectId.zeroId().getName();
+        }
+
+        @Override
+        public String getNewObjectId() {
+          return refObjectId.getName();
+        }
+
+        @Override
+        public boolean isCreate() {
+          return false;
+        }
+
+        @Override
+        public boolean isDelete() {
+          return false;
+        }
+
+        @Override
+        public boolean isNonFastForward() {
+          return false;
+        }
+      };
+    }
+
+    @Override
+    public Set<String> getRefNames() {
+      return Set.of(refName);
+    }
   }
 
   private class FakeProjectDeletedEvent implements ProjectDeletedListener.Event {