Add mirror replication option for JGit client Remove branches and tags when this option is set to true. This option is currently only implemented for JGit. Bug: Issue 40015295 Change-Id: I3aa62c58f36f0d095bff40fe03688677b6a113d3
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/transport/TransportProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/transport/TransportProvider.java index c52db82..ec7f655 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/transport/TransportProvider.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/transport/TransportProvider.java
@@ -56,6 +56,7 @@ throws NotSupportedException, TransportException { Transport tn = Transport.open(local, uri); tn.applyConfig(remoteConfig); + tn.setRemoveDeletedRefs(remoteConfig.isMirror()); if (tn instanceof TransportHttp && bearerToken.isPresent()) { ((TransportHttp) tn) .setAdditionalHeaders(ImmutableMap.of(HDR_AUTHORIZATION, "Bearer " + bearerToken.get()));
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 0c3e02c..b7db2c0 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -523,6 +523,14 @@ By default, false, do *not* replicate project deletions. +remote.NAME.mirror +: If true, replication will remove local branches and tags that are +absent remotely or invisible to the replication (for example read access +denied via `authGroup` option). Note that this option is currently +implemented for the JGit client only. + + By default, false, do not remove remote branches or tags. + remote.NAME.authGroup : Specifies the name of a group that the remote should use to access the repositories. Multiple authGroups may be specified
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java index 76ff02b..e5cd33d 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java
@@ -14,11 +14,21 @@ package com.googlesource.gerrit.plugins.replication.pull; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.gerrit.acceptance.GitUtil.deleteRef; +import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; + import com.google.common.collect.Lists; +import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.SkipProjectClone; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.UseLocalDisk; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.entities.Permission; import com.google.gerrit.extensions.config.FactoryModule; +import com.google.inject.Inject; import com.google.inject.Scopes; import com.google.inject.assistedinject.FactoryModuleBuilder; import com.googlesource.gerrit.plugins.replication.AutoReloadSecureCredentialsFactoryDecorator; @@ -30,12 +40,20 @@ import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchFactory; import com.googlesource.gerrit.plugins.replication.pull.fetch.JGitFetch; import com.googlesource.gerrit.plugins.replication.pull.fetch.PermanentTransportException; +import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState; +import java.io.IOException; import java.net.URISyntaxException; +import java.util.List; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.URIish; +import org.junit.Before; import org.junit.Test; @SkipProjectClone @@ -46,6 +64,17 @@ public class JGitFetchIT extends FetchITBase { private static final String TEST_REPLICATION_SUFFIX = "suffix1"; private static final String TEST_TASK_ID = "taskid"; + private static final RefSpec ALL_REFS = new RefSpec("+refs/*:refs/*"); + + @Inject private ProjectOperations projectOperations; + + @Before + public void allowRefDeletion() { + projectOperations + .allProjectsForUpdate() + .add(allow(Permission.DELETE).ref("refs/*").group(adminGroupUuid())) + .update(); + } @Test(expected = PermanentTransportException.class) public void shouldThrowPermanentTransportExceptionWhenRefDoesNotExists() throws Exception { @@ -59,12 +88,67 @@ } } + @Test + public void shouldPruneRefsWhenMirrorIsTrue() throws Exception { + testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); + String branchName = "anyBranch"; + String branchRef = Constants.R_HEADS + branchName; + String tagName = "anyTag"; + String tagRef = Constants.R_TAGS + tagName; + + PushOneCommit.Result branchPush = pushFactory.create(user.newIdent(), testRepo).to(branchRef); + branchPush.assertOkStatus(); + + PushResult tagPush = pushHead(testRepo, tagRef, false, false); + assertOkStatus(tagPush, tagRef); + + try (Repository localRepo = repoManager.openRepository(project)) { + List<RefUpdateState> fetchCreated = fetchAllRefs(localRepo); + assertThat(fetchCreated.toString()) + .contains(new RefUpdateState(branchRef, RefUpdate.Result.NEW).toString()); + assertThat(fetchCreated.toString()) + .contains(new RefUpdateState(tagRef, RefUpdate.Result.NEW).toString()); + + assertThat(getRef(localRepo, branchRef)).isNotNull(); + + PushResult deleteBranchResult = deleteRef(testRepo, branchRef); + assertOkStatus(deleteBranchResult, branchRef); + + PushResult deleteTagResult = deleteRef(testRepo, tagRef); + assertOkStatus(deleteTagResult, tagRef); + + List<RefUpdateState> fetchDeleted = fetchAllRefs(localRepo); + assertThat(fetchDeleted.toString()) + .contains(new RefUpdateState(branchRef, RefUpdate.Result.FORCED).toString()); + assertThat(getRef(localRepo, branchRef)).isNull(); + + assertThat(fetchDeleted.toString()) + .contains(new RefUpdateState(tagRef, RefUpdate.Result.FORCED).toString()); + assertThat(getRef(localRepo, tagRef)).isNull(); + } + } + + private List<RefUpdateState> fetchAllRefs(Repository localRepo) + throws URISyntaxException, IOException { + Fetch fetch = fetchFactory.create(TEST_TASK_ID, new URIish(testRepoPath.toString()), localRepo); + return fetch.fetch(Lists.newArrayList(ALL_REFS)); + } + + private static void assertOkStatus(PushResult result, String ref) { + RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref); + assertThat(refUpdate).isNotNull(); + assertWithMessage(refUpdate.getMessage()) + .that(refUpdate.getStatus()) + .isEqualTo(RemoteRefUpdate.Status.OK); + } + @SuppressWarnings("unused") private static class TestModule extends FactoryModule { @Override protected void configure() { Config cf = new Config(); cf.setInt("remote", "test_config", "timeout", 0); + cf.setBoolean("remote", "test_config", "mirror", true); try { RemoteConfig remoteConfig = new RemoteConfig(cf, "test_config"); SourceConfiguration sourceConfig = new SourceConfiguration(remoteConfig, cf);