Emit refUpdate event upon copy-approvals

Change I493b9d7ee0 refactored the copy-approvals code to make it
multi-threaded and slice-based.

In doing so, it replaced `BatchUpdate.execute()` calls with with
low-level `BatchRefUpdate.execute()` calls, introducing a regression
whereby `refUpdate` events were no longer emitted.

Explicitly fire `refUpdate` events upon the successful copy-approval
update of the change meta ref and introduce a test to ensure this
behavior is kept.

Release-Notes: skip
Forward-Compatible: checked
Change-Id: I21ac623bd0f1b4a493f7e7984a93bd20911cd3d8
diff --git a/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
index 24595674..d5ee143 100644
--- a/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
+++ b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.InternalUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -68,6 +69,7 @@
   private final InternalUser.Factory internalUserFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeNotes.Factory changeNotesFactory;
+  private final GitReferenceUpdated gitRefUpdated;
   private final ListeningExecutorService executor;
 
   private final ConcurrentHashMap<Project.NameKey, List<ReceiveCommand>> pendingRefUpdates =
@@ -88,12 +90,14 @@
       InternalUser.Factory internalUserFactory,
       ApprovalsUtil approvalsUtil,
       ChangeNotes.Factory changeNotesFactory,
+      GitReferenceUpdated gitRefUpdated,
       @FanOutExecutor ExecutorService executor) {
     this.batchUpdateFactory = batchUpdateFactory;
     this.repositoryManager = repositoryManager;
     this.internalUserFactory = internalUserFactory;
     this.approvalsUtil = approvalsUtil;
     this.changeNotesFactory = changeNotesFactory;
+    this.gitRefUpdated = gitRefUpdated;
     this.executor = MoreExecutors.listeningDecorator(executor);
   }
 
@@ -240,6 +244,7 @@
       }
       bu.addCommand(updates);
       RefUpdateUtil.executeChecked(bu, repository);
+      gitRefUpdated.fire(project, bu, null);
 
       finishedRefUpdates.addAndGet(updates.size());
       logProgress();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
index c2f7771..4d1b032 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
@@ -30,9 +31,13 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.approval.RecursiveApprovalCopier;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Module;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -46,6 +51,21 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private RecursiveApprovalCopier recursiveApprovalCopier;
 
+  @Override
+  public Module createModule() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        CopyApprovalsReferenceUpdateListener referenceUpdateListener =
+            new CopyApprovalsReferenceUpdateListener();
+
+        bind(CopyApprovalsReferenceUpdateListener.class).toInstance(referenceUpdateListener);
+        DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+            .toInstance(referenceUpdateListener);
+      }
+    };
+  }
+
   @Test
   public void multipleProjects() throws Exception {
     Project.NameKey secondProject = projectOperations.newProject().name("secondProject").create();
@@ -222,6 +242,37 @@
   }
 
   @Test
+  public void refUpdateNotified() throws Exception {
+    PushOneCommit.Result change = createChange();
+    gApi.changes().id(change.getChangeId()).current().review(ReviewInput.recommend());
+
+    // this amend is a rework so votes will not be copied.
+    amendChange(change.getChangeId());
+
+    // votes don't exist on the new patch-set for all changes.
+    assertThat(gApi.changes().id(change.getChangeId()).current().votes()).isEmpty();
+
+    // change the project config to make the vote that was not copied to be copied once we do the
+    // schema upgrade.
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      u.getConfig().updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyAnyScore(true));
+      u.save();
+    }
+
+    ObjectId metaId = change.getChange().notes().getMetaId();
+    recursiveApprovalCopier.persist(project, null);
+
+    ApprovalInfo vote1 =
+        Iterables.getOnlyElement(
+            gApi.changes().id(change.getChangeId()).current().votes().values());
+    assertThat(vote1.value).isEqualTo(1);
+    assertThat(vote1._accountId).isEqualTo(admin.id().get());
+
+    CopyApprovalsReferenceUpdateListener testListener = testListener();
+    assertThat(testListener.refUpdateFor(metaId)).isTrue();
+  }
+
+  @Test
   public void oneCorruptChange_otherChangesProcessed() throws Exception {
     PushOneCommit.Result good = createChange();
     gApi.changes().id(good.getChangeId()).current().review(ReviewInput.recommend());
@@ -254,4 +305,22 @@
     assertThat(vote1.value).isEqualTo(1);
     assertThat(vote1._accountId).isEqualTo(admin.id().get());
   }
+
+  private CopyApprovalsReferenceUpdateListener testListener() {
+    return server.getTestInjector().getInstance(CopyApprovalsReferenceUpdateListener.class);
+  }
+
+  private static class CopyApprovalsReferenceUpdateListener implements GitReferenceUpdatedListener {
+    private final AtomicLongMap<String> countsByOldObjectId = AtomicLongMap.create();
+
+    @Override
+    public void onGitReferenceUpdated(Event event) {
+      String oldObjectId = event.getOldObjectId();
+      countsByOldObjectId.incrementAndGet(oldObjectId);
+    }
+
+    boolean refUpdateFor(ObjectId metaRef) {
+      return countsByOldObjectId.containsKey(metaRef.getName());
+    }
+  }
 }