Add replication refs-filtering before push Add ability to filter out refs before being pushed for replication to remote instance. This will help to prevent split-brain issue by allowing us to create filter in multi-site plugin to stop the out of sync instance from overriding the changes of the instance that is up to date. Bug: Issue 11175 Change-Id: I99a7cb4fb2f231626d50abc7d90bb3d79c23861a
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java index 6db6674..5794f6e 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ListMultimap; import com.google.common.collect.Sets; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.metrics.Timer1; @@ -113,6 +114,8 @@ private final ReplicationMetrics metrics; private final ProjectCache projectCache; private final AtomicBoolean canceledWhileRunning; + private final TransportFactory transportFactory; + private DynamicItem<ReplicationPushFilter> replicationPushFilter; @Inject PushOne( @@ -127,6 +130,7 @@ ReplicationStateListeners sl, ReplicationMetrics m, ProjectCache pc, + TransportFactory tf, @Assisted Project.NameKey d, @Assisted URIish u) { gitManager = grm; @@ -147,6 +151,12 @@ projectCache = pc; canceledWhileRunning = new AtomicBoolean(false); maxRetries = p.getMaxRetries(); + transportFactory = tf; + } + + @Inject(optional = true) + public void setReplicationPushFilter(DynamicItem<ReplicationPushFilter> replicationPushFilter) { + this.replicationPushFilter = replicationPushFilter; } @Override @@ -437,7 +447,7 @@ private void runImpl() throws IOException, PermissionBackendException { PushResult res; - try (Transport tn = Transport.open(git, uri)) { + try (Transport tn = transportFactory.open(git, uri)) { res = pushVia(tn); } updateStates(res.getRemoteUpdates()); @@ -496,7 +506,12 @@ local = forProject.filter(local, git, RefFilterOptions.builder().setFilterMeta(true).build()); } - return pushAllRefs ? doPushAll(tn, local) : doPushDelta(local); + List<RemoteRefUpdate> remoteUpdatesList = + pushAllRefs ? doPushAll(tn, local) : doPushDelta(local); + + return replicationPushFilter == null || replicationPushFilter.get() == null + ? remoteUpdatesList + : replicationPushFilter.get().filter(projectName.get(), remoteUpdatesList); } private List<RemoteRefUpdate> doPushAll(Transport tn, Map<String, Ref> local)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationExtensionPointModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationExtensionPointModule.java new file mode 100644 index 0000000..b92a54a --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationExtensionPointModule.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.inject.AbstractModule; + +/** + * Gerrit libModule for applying a ref-filter for outgoing replications. + * + * <p>It should be used only when an actual filter is defined, otherwise the default replication + * plugin behaviour will be pushing all refs without any filtering. + */ +public class ReplicationExtensionPointModule extends AbstractModule { + + @Override + protected void configure() { + DynamicItem.itemOf(binder(), ReplicationPushFilter.class); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java index 464e37a..1898a4f 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -68,5 +68,7 @@ bind(SshSessionFactory.class).toProvider(ReplicationSshSessionFactoryProvider.class); bind(AdminApiFactory.class); + + bind(TransportFactory.class).to(TransportFactoryImpl.class).in(Scopes.SINGLETON); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationPushFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationPushFilter.java new file mode 100644 index 0000000..eb6ba90 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationPushFilter.java
@@ -0,0 +1,30 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import java.util.List; +import org.eclipse.jgit.transport.RemoteRefUpdate; + +/** + * Filter that is invoked before list of remote ref updates is pushed to remote instance. + * + * <p>It can be used to filter out unwanted updates. + */ +@ExtensionPoint +public interface ReplicationPushFilter { + + public List<RemoteRefUpdate> filter(String projectName, List<RemoteRefUpdate> remoteUpdatesList); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/TransportFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/TransportFactory.java new file mode 100644 index 0000000..ba14299 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/TransportFactory.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.Transport; +import org.eclipse.jgit.transport.URIish; + +public interface TransportFactory { + + Transport open(Repository local, URIish uri) throws NotSupportedException, TransportException; +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/TransportFactoryImpl.java b/src/main/java/com/googlesource/gerrit/plugins/replication/TransportFactoryImpl.java new file mode 100644 index 0000000..58c1214 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/TransportFactoryImpl.java
@@ -0,0 +1,30 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.Transport; +import org.eclipse.jgit.transport.URIish; + +public class TransportFactoryImpl implements TransportFactory { + + @Override + public Transport open(Repository git, URIish uri) + throws NotSupportedException, TransportException { + return Transport.open(git, uri); + } +}
diff --git a/src/main/resources/Documentation/extension-point.md b/src/main/resources/Documentation/extension-point.md new file mode 100644 index 0000000..345fd8f --- /dev/null +++ b/src/main/resources/Documentation/extension-point.md
@@ -0,0 +1,39 @@ +@PLUGIN@ extension points +============== + +The replication plugin exposes an extension point to allow influencing the behaviour of the replication events from another plugin or a script. +Extension points can be defined from the replication plugin only when it is loaded as [libModule](/config-gerrit.html#gerrit.installModule) and +implemented by another plugin by declaring a `provided` dependency from the replication plugin. + +### Install extension libModule + +The replication plugin's extension points are defined in the `c.g.g.p.r.ReplicationExtensionPointModule` +that needs to be configured as libModule. + +Create a symbolic link from `$GERRIT_SITE/plugins/replication.jar` into `$GERRIT_SITE/lib` +and then add the replication extension module to the `gerrit.config`. + +Example: + +``` +[gerrit] + installModule = com.googlesource.gerrit.plugins.replication.ReplicationExtensionPointModule +``` + +> **NOTE**: Use and configuration of the replication plugin as library module requires a Gerrit server restart and does not support hot plugin install or upgrade. + + +### Extension points + +* `com.googlesource.gerrit.plugins.replication.ReplicationPushFilter` + + Filter out the ref updates pushed to a remote instance. + Only one filter at a time is supported. Filter implementation needs to bind a `DynamicItem`. + + Default: no filtering + + Example: + + ``` + DynamicItem.bind(binder(),ReplicationPushFilter.class).to(ReplicationPushFilterImpl.class); + ```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java new file mode 100644 index 0000000..af065b3 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java
@@ -0,0 +1,375 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import static com.googlesource.gerrit.plugins.replication.RemoteRefUpdateCollectionMatcher.eqRemoteRef; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.getCurrentArguments; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.metrics.Timer1; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.PerThreadRequestScope; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackend.ForProject; +import com.google.gerrit.server.permissions.PermissionBackend.WithUser; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.util.IdGenerator; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import junit.framework.AssertionFailedError; +import org.easymock.IAnswer; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.transport.FetchConnection; +import org.eclipse.jgit.transport.PushConnection; +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.Transport; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.util.FS; +import org.junit.Before; +import org.junit.Test; + +public class PushOneTest { + private static final int TEST_PUSH_TIMEOUT_SECS = 10; + + private GitRepositoryManager gitRepositoryManagerMock; + private Repository repositoryMock; + private PermissionBackend permissionBackendMock; + private PermissionBackend.WithUser withUserMock; + private PermissionBackend.ForProject forProjectMock; + + private Destination destinationMock; + private RemoteConfig remoteConfigMock; + private RefSpec refSpecMock; + private CredentialsFactory credentialsFactory; + private PerThreadRequestScope.Scoper threadRequestScoperMock; + private ReplicationQueue replicationQueueMock; + private IdGenerator idGeneratorMock; + private ReplicationStateListeners replicationStateListenersMock; + private ReplicationMetrics replicationMetricsMock; + private Timer1.Context timerContextMock; + private ProjectCache projectCacheMock; + private TransportFactory transportFactoryMock; + private Transport transportMock; + private FetchConnection fetchConnection; + private PushConnection pushConnection; + private ProjectState projectStateMock; + private RefUpdate refUpdateMock; + + private Project.NameKey projectNameKey; + private URIish urish; + private Map<String, Ref> localRefs; + + private Map<String, Ref> remoteRefs; + private CountDownLatch isCallFinished; + private Ref newLocalRef; + + @Before + public void setup() throws Exception { + projectNameKey = new Project.NameKey("fooProject"); + urish = new URIish("http://foo.com/fooProject.git"); + + newLocalRef = + new ObjectIdRef.Unpeeled( + NEW, "foo", ObjectId.fromString("0000000000000000000000000000000000000001")); + + localRefs = new HashMap<>(); + localRefs.put("fooProject", newLocalRef); + + Ref remoteRef = new ObjectIdRef.Unpeeled(NEW, "foo", ObjectId.zeroId()); + remoteRefs = new HashMap<>(); + remoteRefs.put("fooProject", remoteRef); + + isCallFinished = new CountDownLatch(1); + + setupMocks(); + } + + private void setupMocks() throws Exception { + FileBasedConfig config = new FileBasedConfig(new Config(), new File("/foo"), FS.DETECTED); + config.setString("remote", "Replication", "push", "foo"); + + setupRefUpdateMock(); + setupRepositoryMock(config); + setupGitRepoManagerMock(); + + projectStateMock = createNiceMock(ProjectState.class); + forProjectMock = createNiceMock(ForProject.class); + setupWithUserMock(); + setupPermissionBackedMock(); + + setupDestinationMock(); + + setupRefSpecMock(); + setupRemoteConfigMock(); + + credentialsFactory = createNiceMock(CredentialsFactory.class); + + setupFetchConnectionMock(); + setupPushConnectionMock(); + setupRequestScopeMock(); + replicationQueueMock = createNiceMock(ReplicationQueue.class); + idGeneratorMock = createNiceMock(IdGenerator.class); + replicationStateListenersMock = createNiceMock(ReplicationStateListeners.class); + + timerContextMock = createNiceMock(Timer1.Context.class); + setupReplicationMetricsMock(); + + setupTransportMock(); + + setupProjectCacheMock(); + + replay( + gitRepositoryManagerMock, + refUpdateMock, + repositoryMock, + permissionBackendMock, + destinationMock, + remoteConfigMock, + credentialsFactory, + threadRequestScoperMock, + replicationQueueMock, + idGeneratorMock, + replicationStateListenersMock, + replicationMetricsMock, + projectCacheMock, + timerContextMock, + transportFactoryMock, + projectStateMock, + withUserMock, + forProjectMock, + fetchConnection, + pushConnection, + refSpecMock); + } + + @Test + public void shouldPushAllRefsWhenNoFilters() throws InterruptedException, IOException { + shouldPushAllRefsWithDynamicItemFilter(DynamicItem.itemOf(ReplicationPushFilter.class, null)); + } + + @Test + public void shouldPushAllRefsWhenNoFiltersSetup() throws InterruptedException, IOException { + shouldPushAllRefsWithDynamicItemFilter(null); + } + + private void shouldPushAllRefsWithDynamicItemFilter( + DynamicItem<ReplicationPushFilter> replicationPushFilter) + throws IOException, NotSupportedException, TransportException, InterruptedException { + List<RemoteRefUpdate> expectedUpdates = + Arrays.asList( + new RemoteRefUpdate( + repositoryMock, + newLocalRef.getName(), + newLocalRef.getObjectId(), + "fooProject", + false, + "fooProject", + null)); + + PushResult pushResult = new PushResult(); + + expect(transportMock.push(anyObject(), eqRemoteRef(expectedUpdates))) + .andReturn(pushResult) + .once(); + replay(transportMock); + + PushOne pushOne = createPushOne(replicationPushFilter); + + pushOne.addRef(PushOne.ALL_REFS); + pushOne.run(); + + isCallFinished.await(TEST_PUSH_TIMEOUT_SECS, TimeUnit.SECONDS); + + verify(transportMock); + } + + @Test + public void shouldBlockReplicationUsingPushFilter() throws InterruptedException, IOException { + DynamicItem<ReplicationPushFilter> replicationPushFilter = + DynamicItem.itemOf( + ReplicationPushFilter.class, + new ReplicationPushFilter() { + + @Override + public List<RemoteRefUpdate> filter( + String projectName, List<RemoteRefUpdate> remoteUpdatesList) { + return Collections.emptyList(); + } + }); + + // easymock way to check if method was never called + expect(transportMock.push(anyObject(), anyObject())) + .andThrow(new AssertionFailedError()) + .anyTimes(); + replay(transportMock); + + PushOne pushOne = createPushOne(replicationPushFilter); + + pushOne.addRef(PushOne.ALL_REFS); + pushOne.run(); + + isCallFinished.await(10, TimeUnit.SECONDS); + + verify(transportMock); + } + + private PushOne createPushOne(DynamicItem<ReplicationPushFilter> replicationPushFilter) { + PushOne push = + new PushOne( + gitRepositoryManagerMock, + permissionBackendMock, + destinationMock, + remoteConfigMock, + credentialsFactory, + threadRequestScoperMock, + replicationQueueMock, + idGeneratorMock, + replicationStateListenersMock, + replicationMetricsMock, + projectCacheMock, + transportFactoryMock, + projectNameKey, + urish); + + push.setReplicationPushFilter(replicationPushFilter); + return push; + } + + private void setupProjectCacheMock() throws IOException { + projectCacheMock = createNiceMock(ProjectCache.class); + expect(projectCacheMock.checkedGet(projectNameKey)).andReturn(projectStateMock); + } + + private void setupTransportMock() throws NotSupportedException, TransportException { + transportMock = createNiceMock(Transport.class); + expect(transportMock.openFetch()).andReturn(fetchConnection); + transportFactoryMock = createNiceMock(TransportFactory.class); + expect(transportFactoryMock.open(repositoryMock, urish)).andReturn(transportMock).anyTimes(); + } + + private void setupReplicationMetricsMock() { + replicationMetricsMock = createNiceMock(ReplicationMetrics.class); + expect(replicationMetricsMock.start(anyObject())).andReturn(timerContextMock); + } + + private void setupRequestScopeMock() { + threadRequestScoperMock = createNiceMock(PerThreadRequestScope.Scoper.class); + expect(threadRequestScoperMock.scope(anyObject())) + .andAnswer( + new IAnswer<Callable<Object>>() { + @SuppressWarnings("unchecked") + @Override + public Callable<Object> answer() throws Throwable { + Callable<Object> originalCall = (Callable<Object>) getCurrentArguments()[0]; + return new Callable<Object>() { + + @Override + public Object call() throws Exception { + Object result = originalCall.call(); + isCallFinished.countDown(); + return result; + } + }; + } + }) + .anyTimes(); + } + + private void setupPushConnectionMock() { + pushConnection = createNiceMock(PushConnection.class); + expect(pushConnection.getRefsMap()).andReturn(remoteRefs); + } + + private void setupFetchConnectionMock() { + fetchConnection = createNiceMock(FetchConnection.class); + expect(fetchConnection.getRefsMap()).andReturn(remoteRefs); + } + + private void setupRemoteConfigMock() { + remoteConfigMock = createNiceMock(RemoteConfig.class); + expect(remoteConfigMock.getPushRefSpecs()).andReturn(ImmutableList.of(refSpecMock)); + } + + private void setupRefSpecMock() { + refSpecMock = createNiceMock(RefSpec.class); + expect(refSpecMock.matchSource(anyObject(String.class))).andReturn(true); + expect(refSpecMock.expandFromSource(anyObject(String.class))).andReturn(refSpecMock); + expect(refSpecMock.getDestination()).andReturn("fooProject").anyTimes(); + expect(refSpecMock.isForceUpdate()).andReturn(false).anyTimes(); + } + + private void setupDestinationMock() { + destinationMock = createNiceMock(Destination.class); + expect(destinationMock.requestRunway(anyObject())).andReturn(RunwayStatus.allowed()); + } + + private void setupPermissionBackedMock() { + permissionBackendMock = createNiceMock(PermissionBackend.class); + expect(permissionBackendMock.currentUser()).andReturn(withUserMock); + } + + private void setupWithUserMock() { + withUserMock = createNiceMock(WithUser.class); + expect(withUserMock.project(projectNameKey)).andReturn(forProjectMock); + } + + private void setupGitRepoManagerMock() throws IOException { + gitRepositoryManagerMock = createNiceMock(GitRepositoryManager.class); + expect(gitRepositoryManagerMock.openRepository(projectNameKey)).andReturn(repositoryMock); + } + + @SuppressWarnings("deprecation") + private void setupRepositoryMock(FileBasedConfig config) throws IOException { + repositoryMock = createNiceMock(Repository.class); + expect(repositoryMock.getConfig()).andReturn(config).anyTimes(); + expect(repositoryMock.getAllRefs()).andReturn(localRefs); + expect(repositoryMock.updateRef("fooProject")).andReturn(refUpdateMock); + } + + private void setupRefUpdateMock() { + refUpdateMock = createNiceMock(RefUpdate.class); + expect(refUpdateMock.getOldObjectId()) + .andReturn(ObjectId.fromString("0000000000000000000000000000000000000001")) + .anyTimes(); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RemoteRefUpdateCollectionMatcher.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RemoteRefUpdateCollectionMatcher.java new file mode 100644 index 0000000..111a792 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/RemoteRefUpdateCollectionMatcher.java
@@ -0,0 +1,64 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import java.util.Collection; +import java.util.Objects; +import org.easymock.EasyMock; +import org.easymock.IArgumentMatcher; +import org.eclipse.jgit.transport.RemoteRefUpdate; + +public class RemoteRefUpdateCollectionMatcher implements IArgumentMatcher { + Collection<RemoteRefUpdate> expectedRemoteRefs; + + public static Collection<RemoteRefUpdate> eqRemoteRef( + Collection<RemoteRefUpdate> expectedRemoteRefs) { + EasyMock.reportMatcher(new RemoteRefUpdateCollectionMatcher(expectedRemoteRefs)); + return null; + } + + public RemoteRefUpdateCollectionMatcher(Collection<RemoteRefUpdate> expectedRemoteRefs) { + this.expectedRemoteRefs = expectedRemoteRefs; + } + + @Override + public boolean matches(Object argument) { + if (!(argument instanceof Collection)) return false; + + @SuppressWarnings("unchecked") + Collection<RemoteRefUpdate> refs = (Collection<RemoteRefUpdate>) argument; + + if (expectedRemoteRefs.size() != refs.size()) return false; + return refs.stream() + .allMatch( + ref -> expectedRemoteRefs.stream().anyMatch(expectedRef -> compare(ref, expectedRef))); + } + + @Override + public void appendTo(StringBuffer buffer) { + buffer.append("expected:" + expectedRemoteRefs.toString()); + } + + private boolean compare(RemoteRefUpdate ref, RemoteRefUpdate expectedRef) { + return Objects.equals(ref.getRemoteName(), expectedRef.getRemoteName()) + && Objects.equals(ref.getStatus(), expectedRef.getStatus()) + && Objects.equals(ref.getExpectedOldObjectId(), expectedRef.getExpectedOldObjectId()) + && Objects.equals(ref.getNewObjectId(), expectedRef.getNewObjectId()) + && Objects.equals(ref.isFastForward(), expectedRef.isFastForward()) + && Objects.equals(ref.getSrcRef(), expectedRef.getSrcRef()) + && Objects.equals(ref.isForceUpdate(), expectedRef.isForceUpdate()) + && Objects.equals(ref.getMessage(), expectedRef.getMessage()); + } +}