Merge branch 'stable-3.12' into stable-3.13

* stable-3.12:
  Add --remote switch to replication start command

Change-Id: Ifa94395155956f48dae777e0e41222a79554afbd
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
index fc80781..c779857 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
@@ -22,6 +22,7 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
 import com.googlesource.gerrit.plugins.replication.api.ReplicationConfig;
+import java.util.Set;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -53,7 +54,7 @@
       ReplicationState state = new ReplicationState(new GitUpdateProcessing(eventDispatcher.get()));
       pushAllFuture.set(
           pushAll
-              .create(null, ReplicationFilter.all(), state, false)
+              .create(null, Set.of(), ReplicationFilter.all(), state, false)
               .schedule(30, TimeUnit.SECONDS));
     }
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
index 4f60319..8958a2a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import java.util.Set;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
@@ -27,13 +28,19 @@
   private final ReplicationStateListener stateLog;
 
   public interface Factory {
-    PushAll create(String urlMatch, ReplicationFilter filter, ReplicationState state, boolean now);
+    PushAll create(
+        String urlMatch,
+        Set<String> remotesToConsider,
+        ReplicationFilter filter,
+        ReplicationState state,
+        boolean now);
   }
 
   private final WorkQueue workQueue;
   private final ProjectCache projectCache;
   private final ReplicationQueue replication;
   private final String urlMatch;
+  private final Set<String> remotesToConsider;
   private final ReplicationFilter filter;
   private final ReplicationState state;
   private final boolean now;
@@ -45,6 +52,7 @@
       ReplicationQueue rq,
       ReplicationStateListeners stateLog,
       @Assisted @Nullable String urlMatch,
+      @Assisted Set<String> remotesToConsider,
       @Assisted ReplicationFilter filter,
       @Assisted ReplicationState state,
       @Assisted boolean now) {
@@ -53,6 +61,7 @@
     this.replication = rq;
     this.stateLog = stateLog;
     this.urlMatch = urlMatch;
+    this.remotesToConsider = remotesToConsider;
     this.filter = filter;
     this.state = state;
     this.now = now;
@@ -67,7 +76,7 @@
     try {
       for (Project.NameKey nameKey : projectCache.all()) {
         if (filter.matches(nameKey)) {
-          replication.scheduleFullSync(nameKey, urlMatch, state, now);
+          replication.scheduleFullSync(nameKey, urlMatch, remotesToConsider, state, now);
         }
       }
     } catch (Exception e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
index 6a36add..7b6079a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -132,9 +132,19 @@
 
   public void scheduleFullSync(
       Project.NameKey project, String urlMatch, ReplicationState state, boolean now) {
+    scheduleFullSync(project, urlMatch, Set.of(), state, now);
+  }
+
+  public void scheduleFullSync(
+      Project.NameKey project,
+      String urlMatch,
+      Set<String> remotesToConsider,
+      ReplicationState state,
+      boolean now) {
     fire(
         project,
         urlMatch,
+        remotesToConsider,
         Set.of(new GitReferenceUpdated.UpdatedRef(PushOne.ALL_REFS, null, null, null)),
         state,
         now);
@@ -157,6 +167,16 @@
       Set<UpdatedRef> updatedRefs,
       ReplicationState state,
       boolean now) {
+    fire(project, urlMatch, Set.of(), updatedRefs, state, now);
+  }
+
+  private void fire(
+      Project.NameKey project,
+      String urlMatch,
+      Set<String> remotesToConsider,
+      Set<UpdatedRef> updatedRefs,
+      ReplicationState state,
+      boolean now) {
     if (!running) {
       stateLog.warn(
           "Replication plugin did not finish startup before event, event replication is postponed",
@@ -166,13 +186,15 @@
     }
 
     for (Destination cfg : destinations.get().getAll(FilterType.ALL)) {
-      pushReferences(
-          cfg,
-          project,
-          urlMatch,
-          updatedRefs.stream().map(UpdatedRef::getRefName).collect(Collectors.toSet()),
-          state,
-          now);
+      if (remotesToConsider.isEmpty() || remotesToConsider.contains(cfg.getRemoteConfigName())) {
+        pushReferences(
+            cfg,
+            project,
+            urlMatch,
+            updatedRefs.stream().map(UpdatedRef::getRefName).collect(Collectors.toSet()),
+            state,
+            now);
+      }
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
index f63df98..507774f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
@@ -20,7 +20,9 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.replication.PushResultProcessing.CommandProcessing;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
@@ -40,6 +42,9 @@
   @Option(name = "--url", metaVar = "PATTERN", usage = "pattern to match URL on")
   private String urlMatch;
 
+  @Option(name = "--remote", metaVar = "REMOTE", usage = "name of remote to replicate to")
+  private Set<String> remotesToConsider = new HashSet<>();
+
   @Option(name = "--wait", usage = "wait for replication to finish before exiting")
   private boolean wait;
 
@@ -70,7 +75,9 @@
     }
 
     Future<?> future =
-        pushFactory.create(urlMatch, projectFilter, state, now).schedule(0, TimeUnit.SECONDS);
+        pushFactory
+            .create(urlMatch, remotesToConsider, projectFilter, state, now)
+            .schedule(0, TimeUnit.SECONDS);
 
     if (wait) {
       if (future != null) {
diff --git a/src/main/resources/Documentation/cmd-start.md b/src/main/resources/Documentation/cmd-start.md
index e1b7341..e6d9fbd 100644
--- a/src/main/resources/Documentation/cmd-start.md
+++ b/src/main/resources/Documentation/cmd-start.md
@@ -12,6 +12,7 @@
 ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start
   [--now]
   [--wait]
+  [--remote <REMOTE> ...]
   {--url <PATTERN> | [--url <PATTERN>] --all | [--url <PATTERN>] <PROJECT PATTERN> ...}
 ```
 
@@ -102,6 +103,12 @@
 URL contains `PATTERN`. This can be useful to replicate only to
 a previously down node, which has been brought back online.
 
+`--remote <REMOTE>`
+: Replicate only to the destination whose remote name exactly matches
+`REMOTE`. May be specified multiple times to target several remotes.
+Remote names correspond to the `[remote "name"]` sections in
+`replication.config`.
+
 EXAMPLES
 --------
 Replicate every project, to every configured remote:
@@ -142,6 +149,18 @@
   $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start --url https://example.com/tools/gerrit.git
 ```
 
+Replicate all projects to a specific named remote:
+
+```console
+  $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start --remote replica1 --all
+```
+
+Replicate all projects to two named remotes:
+
+```console
+  $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start --remote replica1 --remote replica2 --all
+```
+
 SEE ALSO
 --------
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
index 45cf5a7..5013014 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
@@ -34,6 +34,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -281,7 +282,8 @@
         plugin
             .getSysInjector()
             .getInstance(PushAll.Factory.class)
-            .create(null, new ReplicationFilter(Arrays.asList(project.get())), state, false)
+            .create(
+                null, Set.of(), new ReplicationFilter(Arrays.asList(project.get())), state, false)
             .schedule(0, TimeUnit.SECONDS);
 
     future.get();
@@ -301,7 +303,8 @@
         plugin
             .getSysInjector()
             .getInstance(PushAll.Factory.class)
-            .create(null, new ReplicationFilter(Arrays.asList(project.get())), state, false)
+            .create(
+                null, Set.of(), new ReplicationFilter(Arrays.asList(project.get())), state, false)
             .schedule(0, TimeUnit.SECONDS);
 
     CountDownLatch latch = new CountDownLatch(1);
@@ -492,6 +495,51 @@
     }
   }
 
+  @Test
+  public void shouldReplicateToMatchingRemote() throws Exception {
+    Project.NameKey targetProject = createTestProject(project + "replica");
+
+    setReplicationDestination("foo", "replica", ALL_PROJECTS);
+    reloadConfig();
+
+    String newRef = "refs/heads/newForTest";
+    ObjectId newRefTip = createNewBranchWithoutPush("refs/heads/master", newRef);
+
+    plugin
+        .getSysInjector()
+        .getInstance(ReplicationQueue.class)
+        .scheduleFullSync(project, null, Set.of("foo"), new ReplicationState(NO_OP), true);
+
+    try (Repository repo = repoManager.openRepository(targetProject)) {
+      waitUntil(() -> checkedGetRef(repo, newRef) != null);
+
+      Ref targetBranchRef = getRef(repo, newRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(newRefTip);
+    }
+  }
+
+  @Test
+  public void shouldNotReplicateToNonMatchingRemote() throws Exception {
+    Project.NameKey targetProject = createTestProject(project + "replica");
+
+    setReplicationDestination("foo", "replica", ALL_PROJECTS);
+    reloadConfig();
+
+    String newRef = "refs/heads/newForTest";
+    createNewBranchWithoutPush("refs/heads/master", newRef);
+
+    plugin
+        .getSysInjector()
+        .getInstance(ReplicationQueue.class)
+        .scheduleFullSync(project, null, Set.of("bar"), new ReplicationState(NO_OP), true);
+
+    try (Repository repo = repoManager.openRepository(targetProject)) {
+      assertThrows(
+          InterruptedException.class, () -> waitUntil(() -> checkedGetRef(repo, newRef) != null));
+    }
+  }
+
   private void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException {
     WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT);
   }