Add --ref option to start command Users can now trigger replication for a specific reference using the --ref <RefName> option. This allows for targeted replication of individual branches or tags rather than forcing a full project sync. Example usage: ssh -p 29418 host replication start --ref refs/heads/master my-project Change-Id: If6d208411e2a1645fff4f7a6d2e6c5428b87e3f3 Release-Notes: Add --ref <RefName> option to start command
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