Preserve refs order in the GitBatchRefUpdateEvent event

Change I79eefbee introduced the concept of the single event
representing batch ref update. However this new event doesn't
preserve original order of the updated refs. This impacts event
consumers behaviour and can cause failures. For example if meta
ref is processed before patchset ref indexing operation will
fail because of the missing patchset.

To make sure that refs order is preserved and backward compatibility
is kept use LinkedHashSet instead of HashSet.

Bug: Issue 289321387
Release-Notes: Preserve refs order in the GitBatchRefUpdateEvent event
Change-Id: I198d60bb172edbccce739054da93e9f014c02c2b
diff --git a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 814390b..107e987 100644
--- a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -128,7 +128,7 @@
     if (batchRefUpdateListeners.isEmpty() && refUpdatedListeners.isEmpty()) {
       return;
     }
-    Set<GitBatchRefUpdateListener.UpdatedRef> updates = new HashSet<>();
+    Set<GitBatchRefUpdateListener.UpdatedRef> updates = new LinkedHashSet<>();
     for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
       if (cmd.getResult() == ReceiveCommand.Result.OK) {
         updates.add(
@@ -254,7 +254,7 @@
     public Set<String> getRefNames() {
       return updatedRefs.stream()
           .map(GitBatchRefUpdateListener.UpdatedRef::getRefName)
-          .collect(Collectors.toSet());
+          .collect(Collectors.toCollection(LinkedHashSet::new));
     }
 
     @Override
diff --git a/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java b/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
index 96919be..9143dd5f 100644
--- a/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
+++ b/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated.GitBatchRefUpdateEvent;
 import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import java.io.IOException;
@@ -32,6 +35,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.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -45,6 +50,7 @@
   @Mock GitBatchRefUpdateListener batchRefUpdateListener;
   @Mock EventUtil util;
   @Mock AccountState updater;
+  @Captor ArgumentCaptor<GitBatchRefUpdateEvent> eventCaptor;
 
   @Before
   public void setup() {
@@ -83,6 +89,36 @@
   }
 
   @Test
+  public void shouldPreserveRefsOrderInTheBatchRefsUpdatedEvent() {
+    BatchRefUpdate update = newBatchRefUpdate();
+    ReceiveCommand cmd1 =
+        new ReceiveCommand(
+            ObjectId.zeroId(),
+            ObjectId.fromString("0000000000000000000000000000000000000001"),
+            "refs/changes/01/1/1");
+    ReceiveCommand cmd2 =
+        new ReceiveCommand(
+            ObjectId.zeroId(),
+            ObjectId.fromString("0000000000000000000000000000000000000001"),
+            "refs/changes/01/1/meta");
+    cmd1.setResult(ReceiveCommand.Result.OK);
+    cmd2.setResult(ReceiveCommand.Result.OK);
+    update.addCommand(cmd1);
+    update.addCommand(cmd2);
+
+    GitReferenceUpdated event =
+        new GitReferenceUpdated(
+            new PluginSetContext<>(batchRefUpdateListeners, PluginMetrics.DISABLED_INSTANCE),
+            new PluginSetContext<>(refUpdatedListeners, PluginMetrics.DISABLED_INSTANCE),
+            util);
+    event.fire(Project.NameKey.parse("project"), update, updater);
+    Mockito.verify(batchRefUpdateListener, Mockito.times(1))
+        .onGitBatchRefUpdate(eventCaptor.capture());
+    GitBatchRefUpdateEvent batchEvent = eventCaptor.getValue();
+    assertThat(batchEvent.getRefNames()).isInOrder();
+  }
+
+  @Test
   public void RefUpdateEventAndRefsUpdateEventAreFired_RefUpdate() throws Exception {
     String ref = "refs/heads/master";
     RefUpdate update = newRefUpdate(ref);