Merge "Retry failovers by applying url distribution strategy"
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 ecc55a2..2bf2ae4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
@@ -53,7 +53,7 @@
       ReplicationState state = new ReplicationState(new GitUpdateProcessing(eventDispatcher));
       pushAllFuture.set(
           pushAll
-              .create(null, Set.of(), ReplicationFilter.all(), state, false)
+              .create(null, PushOne.ALL_REFS, 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 8958a2a..19ae420 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
@@ -29,7 +29,8 @@
 
   public interface Factory {
     PushAll create(
-        String urlMatch,
+        @Assisted("urlMatch") String urlMatch,
+        @Assisted("refName") String refName,
         Set<String> remotesToConsider,
         ReplicationFilter filter,
         ReplicationState state,
@@ -41,6 +42,7 @@
   private final ReplicationQueue replication;
   private final String urlMatch;
   private final Set<String> remotesToConsider;
+  private final String refName;
   private final ReplicationFilter filter;
   private final ReplicationState state;
   private final boolean now;
@@ -51,7 +53,8 @@
       ProjectCache projectCache,
       ReplicationQueue rq,
       ReplicationStateListeners stateLog,
-      @Assisted @Nullable String urlMatch,
+      @Assisted("urlMatch") @Nullable String urlMatch,
+      @Assisted("refName") String refName,
       @Assisted Set<String> remotesToConsider,
       @Assisted ReplicationFilter filter,
       @Assisted ReplicationState state,
@@ -61,6 +64,7 @@
     this.replication = rq;
     this.stateLog = stateLog;
     this.urlMatch = urlMatch;
+    this.refName = refName;
     this.remotesToConsider = remotesToConsider;
     this.filter = filter;
     this.state = state;
@@ -76,7 +80,7 @@
     try {
       for (Project.NameKey nameKey : projectCache.all()) {
         if (filter.matches(nameKey)) {
-          replication.scheduleFullSync(nameKey, urlMatch, remotesToConsider, state, now);
+          replication.scheduleFullSync(nameKey, urlMatch, refName, remotesToConsider, state, now);
         }
       }
     } catch (Exception e) {
@@ -87,7 +91,9 @@
 
   @Override
   public String toString() {
-    String s = "Replicate All Projects";
+    String refs = PushOne.ALL_REFS.equals(refName) ? "All Refs" : "[" + refName + "]";
+    String s = "Replicate " + refs + " for All Projects";
+
     if (urlMatch != null) {
       s = s + " to " + urlMatch;
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RepairCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RepairCommand.java
index 0371c9c..dd5e506 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/RepairCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RepairCommand.java
@@ -110,6 +110,7 @@
             "\nRunning replication start for " + project.get() + " to " + uri.toString() + " ...");
         replicationStarter.start(
             uri.toString(),
+            PushOne.ALL_REFS,
             Set.of(),
             new ReplicationFilter(List.of(project.get()), Collections.emptyList()),
             /* now= */ true,
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 2652cb9..59acb20 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -130,13 +130,9 @@
   }
 
   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,
+      String refName,
       Set<String> remotesToConsider,
       ReplicationState state,
       boolean now) {
@@ -144,7 +140,7 @@
         project,
         urlMatch,
         remotesToConsider,
-        Set.of(new GitReferenceUpdated.UpdatedRef(PushOne.ALL_REFS, null, null, null)),
+        Set.of(new GitReferenceUpdated.UpdatedRef(refName, null, null, null)),
         state,
         now);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java
index b7baab9..7202eab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java
@@ -37,6 +37,7 @@
 
   void start(
       @Nullable String urlMatch,
+      String refName,
       Set<String> remotesToConsider,
       ReplicationFilter filter,
       boolean now,
@@ -46,7 +47,7 @@
 
     Future<?> future =
         pushFactory
-            .create(urlMatch, remotesToConsider, filter, state, now)
+            .create(urlMatch, refName, remotesToConsider, filter, state, now)
             .schedule(0, TimeUnit.SECONDS);
 
     if (wait) {
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 2c084b1..d6d99a8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
@@ -37,6 +37,9 @@
   @Option(name = "--url", metaVar = "SUBSTRING", usage = "substring URL must match (or * to match everything)")
   private String urlMatch;
 
+  @Option(name = "--ref", metaVar = "RefName", usage = "ref to replicate")
+  private String refName = PushOne.ALL_REFS;
+
   private final Set<String> remotesToConsider = new HashSet<>();
 
   @Option(name = "--remote", metaVar = "REMOTE", usage = "name of remote to replicate to")
@@ -68,7 +71,7 @@
             ? ReplicationFilter.all()
             : new ReplicationFilter(projectPatterns, Collections.emptyList());
 
-    replicationStarter.start(urlMatch, remotesToConsider, projectFilter, now, wait, this);
+    replicationStarter.start(urlMatch, refName, remotesToConsider, projectFilter, now, wait, this);
   }
 
   @Override
diff --git a/src/main/resources/Documentation/cmd-start.md b/src/main/resources/Documentation/cmd-start.md
index e6d9fbd..47989a1 100644
--- a/src/main/resources/Documentation/cmd-start.md
+++ b/src/main/resources/Documentation/cmd-start.md
@@ -13,6 +13,7 @@
   [--now]
   [--wait]
   [--remote <REMOTE> ...]
+  [--ref <RefName>]
   {--url <PATTERN> | [--url <PATTERN>] --all | [--url <PATTERN>] <PROJECT PATTERN> ...}
 ```
 
@@ -97,6 +98,10 @@
 `--all`
 : Schedule replication for all projects.
 
+`--ref <RefName>`
+: Replicate only the single reference specified by `<RefName>`.
+If omitted, the command defaults to all references (`refs/*`).
+
 `--url <PATTERN>`
 : Replicate only to replication destinations whose configuration
 URL contains the substring `PATTERN`, or whose expanded project
@@ -131,6 +136,12 @@
   $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start tools/gerrit
 ```
 
+Replicate only the `master` branch of the `tools/gerrit` project:
+
+```console
+  $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start --ref refs/heads/master tools/gerrit
+```
+
 Replicate only projects located in the `documentation` subdirectory:
 
 ```console
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 d436f42..7ff0f3b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
@@ -189,6 +189,30 @@
   }
 
   @Test
+  public void shouldReplicateOnlySpecificRef() throws Exception {
+    Project.NameKey targetProject = createTestProject(project + "replica");
+
+    setReplicationDestination("foo", "replica", ALL_PROJECTS);
+    reloadConfig();
+
+    String branch1 = "refs/heads/branch1";
+    String branch2 = "refs/heads/branch2";
+    createNewBranchWithoutPush("refs/heads/master", branch1);
+    createNewBranchWithoutPush("refs/heads/master", branch2);
+
+    plugin
+        .getSysInjector()
+        .getInstance(ReplicationQueue.class)
+        .scheduleFullSync(project, null, branch1, Set.of(), new ReplicationState(NO_OP), true);
+
+    try (Repository repo = repoManager.openRepository(targetProject)) {
+      waitUntil(() -> checkedGetRef(repo, branch1) != null);
+      assertThat(getRef(repo, branch1)).isNotNull();
+      assertThat(getRef(repo, branch2)).isNull();
+    }
+  }
+
+  @Test
   public void shouldReplicateNewBranchToTwoRemotes() throws Exception {
     Project.NameKey targetProject1 = createTestProject(project + "replica1");
     Project.NameKey targetProject2 = createTestProject(project + "replica2");
@@ -232,7 +256,7 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, urlMatch, new ReplicationState(NO_OP), true);
+        .scheduleFullSync(project, urlMatch, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), true);
 
     try (Repository repo = repoManager.openRepository(targetProject)) {
       waitUntil(() -> checkedGetRef(repo, newRef) != null);
@@ -258,7 +282,7 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, urlMatch, new ReplicationState(NO_OP), true);
+        .scheduleFullSync(project, urlMatch, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), true);
 
     try (Repository repo = repoManager.openRepository(targetProject)) {
       waitUntil(() -> checkedGetRef(repo, newRef) != null);
@@ -284,6 +308,7 @@
             .getInstance(PushAll.Factory.class)
             .create(
                 null,
+                PushOne.ALL_REFS,
                 Set.of(),
                 new ReplicationFilter(Arrays.asList(project.get()), null),
                 state,
@@ -309,6 +334,7 @@
             .getInstance(PushAll.Factory.class)
             .create(
                 null,
+                PushOne.ALL_REFS,
                 Set.of(),
                 new ReplicationFilter(Arrays.asList(project.get()), null),
                 state,
@@ -518,7 +544,7 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, null, new ReplicationState(NO_OP), true);
+        .scheduleFullSync(project, null, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), true);
 
     // Wait for the push to land on both the refs
     try (Repository r1 = repoManager.openRepository(replica1Project);
@@ -543,7 +569,7 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, null, new ReplicationState(NO_OP), true);
+        .scheduleFullSync(project, null, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), true);
 
     // Wait for the push to land in at least one replica
     try (Repository r1 = repoManager.openRepository(replica1Project);
@@ -574,7 +600,7 @@
     ReplicationQueue queue = plugin.getSysInjector().getInstance(ReplicationQueue.class);
 
     // First sync - goes to replica1 (index 0)
-    queue.scheduleFullSync(project, null, new ReplicationState(NO_OP), true);
+    queue.scheduleFullSync(project, null, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), true);
 
     try (Repository r1 = repoManager.openRepository(replica1Project)) {
       waitUntil(() -> checkedGetRef(r1, branch1) != null);
@@ -587,7 +613,7 @@
 
     // Second sync - goes to replica2 (index 1), includes branch1 and branch2
     createNewBranchWithoutPush("refs/heads/master", branch2);
-    queue.scheduleFullSync(project, null, new ReplicationState(NO_OP), true);
+    queue.scheduleFullSync(project, null, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), true);
 
     try (Repository r2 = repoManager.openRepository(replica2Project)) {
       waitUntil(() -> checkedGetRef(r2, branch1) != null && checkedGetRef(r2, branch2) != null);
@@ -612,7 +638,7 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, null, Set.of("foo"), new ReplicationState(NO_OP), true);
+        .scheduleFullSync(project, null, PushOne.ALL_REFS, Set.of("foo"), new ReplicationState(NO_OP), true);
 
     try (Repository repo = repoManager.openRepository(targetProject)) {
       waitUntil(() -> checkedGetRef(repo, newRef) != null);
@@ -670,10 +696,10 @@
 
     ReplicationQueue replicationQueue = plugin.getSysInjector().getInstance(ReplicationQueue.class);
     ReplicationState state = new ReplicationState(NO_OP);
-    replicationQueue.scheduleFullSync(prj1, null, Set.of("foo"), state, true);
-    replicationQueue.scheduleFullSync(prj2, null, Set.of("foo"), state, true);
-    replicationQueue.scheduleFullSync(prj3, null, Set.of("foo"), state, true);
-    replicationQueue.scheduleFullSync(prj4, null, Set.of("foo"), state, true);
+    replicationQueue.scheduleFullSync(prj1, null, PushOne.ALL_REFS, Set.of("foo"), state, true);
+    replicationQueue.scheduleFullSync(prj2, null, PushOne.ALL_REFS, Set.of("foo"), state, true);
+    replicationQueue.scheduleFullSync(prj3, null, PushOne.ALL_REFS, Set.of("foo"), state, true);
+    replicationQueue.scheduleFullSync(prj4, null, PushOne.ALL_REFS, Set.of("foo"), state, true);
 
     try (Repository excludeRepo1 = repoManager.openRepository(targetPrj1);
         Repository excludeRepo2 = repoManager.openRepository(targetPrj2);
@@ -725,7 +751,8 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, null, Set.of("bar"), new ReplicationState(NO_OP), true);
+        .scheduleFullSync(
+            project, null, PushOne.ALL_REFS, Set.of("bar"), new ReplicationState(NO_OP), true);
 
     try (Repository repo = repoManager.openRepository(targetProject)) {
       assertThrows(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStorageIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStorageIT.java
index 643a781..d0affe2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStorageIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStorageIT.java
@@ -76,12 +76,36 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, null, new ReplicationState(NO_OP), false);
+        .scheduleFullSync(project, null, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), false);
 
     assertThat(listWaitingReplicationTasks(Pattern.quote(PushOne.ALL_REFS))).hasSize(1);
   }
 
   @Test
+  public void shouldCreateOneReplicationTaskWhenSchedulingSpecificRefSync() throws Exception {
+    createTestProject(project + "replica");
+
+    setReplicationDestination("foo", "replica", ALL_PROJECTS, Integer.MAX_VALUE);
+    reloadConfig();
+
+    String specificRef = "refs/heads/master";
+
+    plugin
+        .getSysInjector()
+        .getInstance(ReplicationQueue.class)
+        .scheduleFullSync(project, null, specificRef, Set.of(), new ReplicationState(NO_OP), false);
+
+    assertThat(listWaitingReplicationTasks(Pattern.quote(specificRef))).hasSize(1);
+
+    tasksStorage
+        .streamWaiting()
+        .forEach(
+            (task) -> {
+              assertThat(task.refs()).containsExactly(specificRef);
+            });
+  }
+
+  @Test
   public void shouldFirePendingOnlyToIncompleteUri() throws Exception {
     String suffix1 = "replica1";
     String suffix2 = "replica2";
@@ -197,7 +221,7 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, urlMatch, new ReplicationState(NO_OP), false);
+        .scheduleFullSync(project, urlMatch, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), false);
 
     assertThat(listWaiting()).hasSize(1);
     tasksStorage
@@ -222,7 +246,7 @@
     plugin
         .getSysInjector()
         .getInstance(ReplicationQueue.class)
-        .scheduleFullSync(project, urlMatch, new ReplicationState(NO_OP), false);
+        .scheduleFullSync(project, urlMatch, PushOne.ALL_REFS, Set.of(), new ReplicationState(NO_OP), false);
 
     assertThat(listWaiting()).hasSize(1);
     tasksStorage