diff options
17 files changed, 468 insertions, 179 deletions
@@ -1,5 +1,5 @@ load("//tools/bzl:junit.bzl", "junit_tests") -load("//tools/bzl:plugin.bzl", "gerrit_plugin") +load("//tools/bzl:plugin.bzl", "gerrit_plugin", "PLUGIN_DEPS", "PLUGIN_TEST_DEPS") gerrit_plugin( name = "replication", @@ -23,11 +23,9 @@ junit_tests( srcs = glob(["src/test/java/**/*Test.java"]), tags = ["replication"], visibility = ["//visibility:public"], - deps = [ + deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [ ":replication__plugin", ":replication_util", - "//gerrit-acceptance-framework:lib", - "//gerrit-plugin-api:lib", ], ) @@ -38,9 +36,7 @@ java_library( ["src/test/java/**/*.java"], exclude = ["src/test/java/**/*Test.java"], ), - deps = [ + deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [ ":replication__plugin", - "//gerrit-acceptance-framework:lib", - "//gerrit-plugin-api:lib", ], ) diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java index b2dd382..0cee37c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java @@ -15,6 +15,8 @@ package com.googlesource.gerrit.plugins.replication; import static com.googlesource.gerrit.plugins.replication.PushResultProcessing.resolveNodeName; +import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.NON_EXISTING; +import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.REJECTED_OTHER_REASON; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; @@ -24,8 +26,10 @@ import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.Lists; import com.google.gerrit.common.EventDispatcher; import com.google.gerrit.common.data.GroupReference; +import com.google.gerrit.extensions.client.ProjectState; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Project; @@ -41,6 +45,10 @@ import com.google.gerrit.server.config.RequestScopedReviewDbProvider; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.PerThreadRequestScope; import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.PerRequestProjectControlCache; import com.google.gerrit.server.project.ProjectControl; @@ -50,13 +58,16 @@ import com.google.inject.Provider; import com.google.inject.Provides; import com.google.inject.assistedinject.FactoryModuleBuilder; import com.google.inject.servlet.RequestScoped; +import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.io.FilenameUtils; import org.eclipse.jgit.lib.Constants; @@ -64,6 +75,7 @@ import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; 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.slf4j.Logger; @@ -76,7 +88,8 @@ public class Destination { private final PushOne.Factory opFactory; private final ProjectControl.Factory projectControlFactory; private final GitRepositoryManager gitManager; - private volatile WorkQueue.Executor pool; + private final PermissionBackend permissionBackend; + private volatile ScheduledExecutorService pool; private final PerThreadRequestScope.Scoper threadScoper; private final DestinationConfiguration config; private final DynamicItem<EventDispatcher> eventDispatcher; @@ -103,6 +116,7 @@ public class Destination { RemoteSiteUser.Factory replicationUserFactory, PluginUser pluginUser, GitRepositoryManager gitRepositoryManager, + PermissionBackend permissionBackend, GroupBackend groupBackend, ReplicationStateListener stateLog, GroupIncludeCache groupIncludeCache, @@ -110,9 +124,10 @@ public class Destination { config = cfg; this.eventDispatcher = eventDispatcher; gitManager = gitRepositoryManager; + this.permissionBackend = permissionBackend; this.stateLog = stateLog; - final CurrentUser remoteUser; + CurrentUser remoteUser; if (!cfg.getAuthGroupNames().isEmpty()) { ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder(); for (String name : cfg.getAuthGroupNames()) { @@ -200,33 +215,54 @@ public class Destination { public int shutdown() { int cnt = 0; if (pool != null) { - for (Runnable r : pool.getQueue()) { - repLog.warn("Cancelling replication event {}", r); - } + repLog.warn("Cancelling replication events"); + cnt = pool.shutdownNow().size(); - pool.unregisterWorkQueue(); pool = null; } return cnt; } - private boolean shouldReplicate(ProjectControl projectControl) { - return projectControl.isReadable() - && (!projectControl.isHidden() || config.replicateHiddenProjects()); + private boolean shouldReplicate(ProjectControl ctl) throws PermissionBackendException { + if (!config.replicateHiddenProjects() && ctl.getProject().getState() == ProjectState.HIDDEN) { + return false; + } + try { + permissionBackend + .user(ctl.getUser()) + .project(ctl.getProject().getNameKey()) + .check(ProjectPermission.ACCESS); + return true; + } catch (AuthException e) { + return false; + } } private boolean shouldReplicate( - final Project.NameKey project, final String ref, ReplicationState... states) { + final Project.NameKey project, String ref, ReplicationState... states) { try { return threadScoper .scope( new Callable<Boolean>() { @Override - public Boolean call() throws NoSuchProjectException { + public Boolean call() throws NoSuchProjectException, PermissionBackendException { ProjectControl projectControl = controlFor(project); - return shouldReplicate(projectControl) - && (PushOne.ALL_REFS.equals(ref) - || projectControl.controlForRef(ref).isVisible()); + if (!shouldReplicate(projectControl)) { + return false; + } + if (PushOne.ALL_REFS.equals(ref)) { + return true; + } + try { + permissionBackend + .user(projectControl.getUser()) + .project(project) + .ref(ref) + .check(RefPermission.READ); + } catch (AuthException e) { + return false; + } + return true; } }) .call(); @@ -239,13 +275,13 @@ public class Destination { return false; } - private boolean shouldReplicate(final Project.NameKey project, ReplicationState... states) { + private boolean shouldReplicate(Project.NameKey project, ReplicationState... states) { try { return threadScoper .scope( new Callable<Boolean>() { @Override - public Boolean call() throws NoSuchProjectException { + public Boolean call() throws NoSuchProjectException, PermissionBackendException { return shouldReplicate(controlFor(project)); } }) @@ -260,6 +296,11 @@ public class Destination { } void schedule(Project.NameKey project, String ref, URIish uri, ReplicationState state) { + schedule(project, ref, uri, state, false); + } + + void schedule( + Project.NameKey project, String ref, URIish uri, ReplicationState state, boolean now) { repLog.info("scheduling replication {}:{} => {}", project, ref, uri); if (!shouldReplicate(project, ref, state)) { return; @@ -296,7 +337,7 @@ public class Destination { e = opFactory.create(project, uri); addRef(e, ref); e.addState(ref, state); - pool.schedule(e, config.getDelay(), TimeUnit.SECONDS); + pool.schedule(e, now ? 0 : config.getDelay(), TimeUnit.SECONDS); pending.put(uri, e); } else if (!e.getRefs().contains(ref)) { addRef(e, ref); @@ -316,7 +357,7 @@ public class Destination { private void addRef(PushOne e, String ref) { e.addRef(ref); - postEvent(e, ref); + postReplicationScheduledEvent(e, ref); } /** @@ -394,7 +435,13 @@ public class Destination { case TRANSPORT_ERROR: case REPOSITORY_MISSING: default: + RemoteRefUpdate.Status status = + RetryReason.REPOSITORY_MISSING.equals(reason) + ? NON_EXISTING + : REJECTED_OTHER_REASON; + postReplicationFailedEvent(pushOp, status); if (pushOp.setToRetry()) { + postReplicationScheduledEvent(pushOp); pool.schedule(pushOp, config.getRetryDelay(), TimeUnit.MINUTES); } else { pushOp.canceledByReplication(); @@ -572,10 +619,36 @@ public class Destination { return uri.toString().contains(urlMatch); } - private void postEvent(PushOne pushOp, String ref) { + private void postReplicationScheduledEvent(PushOne pushOp) { + postReplicationScheduledEvent(pushOp, null); + } + + private void postReplicationScheduledEvent(PushOne pushOp, String inputRef) { + Set<String> refs = inputRef == null ? pushOp.getRefs() : ImmutableSet.of(inputRef); Project.NameKey project = pushOp.getProjectNameKey(); String targetNode = resolveNodeName(pushOp.getURI()); - ReplicationScheduledEvent event = new ReplicationScheduledEvent(project.get(), ref, targetNode); - eventDispatcher.get().postEvent(new Branch.NameKey(project, ref), event); + for (String ref : refs) { + ReplicationScheduledEvent event = + new ReplicationScheduledEvent(project.get(), ref, targetNode); + try { + eventDispatcher.get().postEvent(new Branch.NameKey(project, ref), event); + } catch (PermissionBackendException e) { + repLog.error("error posting event", e); + } + } + } + + private void postReplicationFailedEvent(PushOne pushOp, RemoteRefUpdate.Status status) { + Project.NameKey project = pushOp.getProjectNameKey(); + String targetNode = resolveNodeName(pushOp.getURI()); + for (String ref : pushOp.getRefs()) { + RefReplicatedEvent event = + new RefReplicatedEvent(project.get(), ref, targetNode, RefPushResult.FAILED, status); + try { + eventDispatcher.get().postEvent(new Branch.NameKey(project, ref), event); + } catch (PermissionBackendException e) { + repLog.error("error posting event", e); + } + } } } diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationFactory.java index df886cb..83eab86 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationFactory.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationFactory.java @@ -20,6 +20,7 @@ import com.google.gerrit.server.PluginUser; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupIncludeCache; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; @@ -30,6 +31,7 @@ public class DestinationFactory { private final RemoteSiteUser.Factory replicationUserFactory; private final PluginUser pluginUser; private final GitRepositoryManager gitRepositoryManager; + private final PermissionBackend permissionBackend; private final GroupBackend groupBackend; private final ReplicationStateListener stateLog; private final GroupIncludeCache groupIncludeCache; @@ -41,6 +43,7 @@ public class DestinationFactory { RemoteSiteUser.Factory replicationUserFactory, PluginUser pluginUser, GitRepositoryManager gitRepositoryManager, + PermissionBackend permissionBackend, GroupBackend groupBackend, ReplicationStateListener stateLog, GroupIncludeCache groupIncludeCache, @@ -49,6 +52,7 @@ public class DestinationFactory { this.replicationUserFactory = replicationUserFactory; this.pluginUser = pluginUser; this.gitRepositoryManager = gitRepositoryManager; + this.permissionBackend = permissionBackend; this.groupBackend = groupBackend; this.stateLog = stateLog; this.groupIncludeCache = groupIncludeCache; @@ -62,6 +66,7 @@ public class DestinationFactory { replicationUserFactory, pluginUser, gitRepositoryManager, + permissionBackend, groupBackend, stateLog, groupIncludeCache, diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/GerritSshApi.java b/src/main/java/com/googlesource/gerrit/plugins/replication/GerritSshApi.java new file mode 100644 index 0000000..b46a0d9 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/GerritSshApi.java @@ -0,0 +1,132 @@ +// Copyright (C) 2017 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.reviewdb.client.Project; +import com.google.gerrit.server.ssh.SshAddressesModule; +import com.google.inject.Inject; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; +import org.eclipse.jgit.transport.URIish; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class GerritSshApi { + static int SSH_COMMAND_FAILED = -1; + private static final Logger log = LoggerFactory.getLogger(GerritSshApi.class); + private static String GERRIT_ADMIN_PROTOCOL_PREFIX = "gerrit+"; + + private final SshHelper sshHelper; + + private final Set<URIish> withoutDeleteProjectPlugin = new HashSet<>(); + + @Inject + protected GerritSshApi(SshHelper sshHelper) { + this.sshHelper = sshHelper; + } + + protected boolean createProject(URIish uri, Project.NameKey projectName, String head) { + OutputStream errStream = sshHelper.newErrorBufferStream(); + String cmd = "gerrit create-project --branch " + head + " " + projectName.get(); + try { + execute(uri, cmd, errStream); + } catch (IOException e) { + logError("creating", uri, errStream, cmd, e); + return false; + } + return true; + } + + protected boolean deleteProject(URIish uri, Project.NameKey projectName) { + if (!withoutDeleteProjectPlugin.contains(uri)) { + OutputStream errStream = sshHelper.newErrorBufferStream(); + String cmd = "deleteproject delete --yes-really-delete --force " + projectName.get(); + int exitCode = -1; + try { + exitCode = execute(uri, cmd, errStream); + } catch (IOException e) { + logError("deleting", uri, errStream, cmd, e); + return false; + } + if (exitCode == 1) { + log.info( + "DeleteProject plugin is not installed on {}; will not try to forward this operation to that host"); + withoutDeleteProjectPlugin.add(uri); + return true; + } + } + return true; + } + + protected boolean updateHead(URIish uri, Project.NameKey projectName, String newHead) { + OutputStream errStream = sshHelper.newErrorBufferStream(); + String cmd = "gerrit set-head " + projectName.get() + " --new-head " + newHead; + try { + execute(uri, cmd, errStream); + } catch (IOException e) { + log.error( + "Error updating HEAD of remote repository at {} to {}:\n" + + " Exception: {}\n Command: {}\n Output: {}", + uri, + newHead, + e, + cmd, + errStream, + e); + return false; + } + return true; + } + + private URIish toSshUri(URIish uri) throws URISyntaxException { + String uriStr = uri.toString(); + if (uri.getHost() != null && uriStr.startsWith(GERRIT_ADMIN_PROTOCOL_PREFIX)) { + return new URIish(uriStr.substring(GERRIT_ADMIN_PROTOCOL_PREFIX.length())); + } + String rawPath = uri.getRawPath(); + if (!rawPath.endsWith("/")) { + rawPath = rawPath + "/"; + } + URIish sshUri = new URIish("ssh://" + rawPath); + if (sshUri.getPort() < 0) { + sshUri = sshUri.setPort(SshAddressesModule.DEFAULT_PORT); + } + return sshUri; + } + + private int execute(URIish uri, String cmd, OutputStream errStream) throws IOException { + try { + URIish sshUri = toSshUri(uri); + return sshHelper.executeRemoteSsh(sshUri, cmd, errStream); + } catch (URISyntaxException e) { + log.error("Cannot convert {} to SSH uri", uri, e); + } + return SSH_COMMAND_FAILED; + } + + public void logError(String msg, URIish uri, OutputStream errStream, String cmd, IOException e) { + log.error( + "Error {} remote repository at {}:\n Exception: {}\n Command: {}\n Output: {}", + msg, + uri, + e, + cmd, + errStream, + e); + } +} 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 a6b38c1..227804d 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java @@ -56,7 +56,9 @@ public class OnStartStop implements LifecycleListener { && config.isReplicateAllOnPluginStart()) { ReplicationState state = new ReplicationState(new GitUpdateProcessing(eventDispatcher.get())); pushAllFuture.set( - pushAll.create(null, ReplicationFilter.all(), state).schedule(30, TimeUnit.SECONDS)); + pushAll + .create(null, 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 da32ecd..db067e2 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java @@ -27,7 +27,7 @@ public class PushAll implements Runnable { private final ReplicationStateListener stateLog; public interface Factory { - PushAll create(String urlMatch, ReplicationFilter filter, ReplicationState state); + PushAll create(String urlMatch, ReplicationFilter filter, ReplicationState state, boolean now); } private final WorkQueue workQueue; @@ -36,6 +36,7 @@ public class PushAll implements Runnable { private final String urlMatch; private final ReplicationFilter filter; private final ReplicationState state; + private final boolean now; @Inject protected PushAll( @@ -45,7 +46,8 @@ public class PushAll implements Runnable { ReplicationStateListener stateLog, @Assisted @Nullable String urlMatch, @Assisted ReplicationFilter filter, - @Assisted ReplicationState state) { + @Assisted ReplicationState state, + @Assisted boolean now) { this.workQueue = wq; this.projectCache = projectCache; this.replication = rq; @@ -53,6 +55,7 @@ public class PushAll implements Runnable { this.urlMatch = urlMatch; this.filter = filter; this.state = state; + this.now = now; } Future<?> schedule(long delay, TimeUnit unit) { @@ -64,7 +67,7 @@ public class PushAll implements Runnable { try { for (Project.NameKey nameKey : projectCache.all()) { if (filter.matches(nameKey)) { - replication.scheduleFullSync(nameKey, urlMatch, state); + replication.scheduleFullSync(nameKey, urlMatch, state, now); } } } catch (Exception e) { 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 64f5152..1efad4f 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java @@ -21,25 +21,22 @@ import com.google.common.base.Throwables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Sets; -import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.metrics.Timer1; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.PerThreadRequestScope; import com.google.gerrit.server.git.ProjectRunnable; -import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.WorkQueue.CanceledWhileRunning; -import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.util.IdGenerator; -import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult; @@ -90,14 +87,12 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { } private final GitRepositoryManager gitManager; - private final SchemaFactory<ReviewDb> schema; + private final PermissionBackend permissionBackend; private final Destination pool; private final RemoteConfig config; private final CredentialsProvider credentialsProvider; - private final TagCache tagCache; private final PerThreadRequestScope.Scoper threadScoper; - private final ChangeNotes.Factory changeNotesFactory; - private final SearchingChangeCacheImpl changeCache; + private final VisibleRefFilter.Factory refFilterFactory; private final ReplicationQueue replicationQueue; private final Project.NameKey projectName; @@ -120,14 +115,12 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { @Inject PushOne( GitRepositoryManager grm, - SchemaFactory<ReviewDb> s, + PermissionBackend permissionBackend, Destination p, RemoteConfig c, + VisibleRefFilter.Factory rff, CredentialsFactory cpFactory, - TagCache tc, PerThreadRequestScope.Scoper ts, - ChangeNotes.Factory nf, - @Nullable SearchingChangeCacheImpl cc, ReplicationQueue rq, IdGenerator ig, ReplicationStateListener sl, @@ -135,14 +128,12 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { @Assisted Project.NameKey d, @Assisted URIish u) { gitManager = grm; - schema = s; + this.permissionBackend = permissionBackend; pool = p; config = c; + refFilterFactory = rff; credentialsProvider = cpFactory.create(c.getName()); - tagCache = tc; threadScoper = ts; - changeNotesFactory = nf; - changeCache = cc; replicationQueue = rq; projectName = d; uri = u; @@ -338,7 +329,9 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { // does not exist. In this case NoRemoteRepositoryException is not // raised. String msg = e.getMessage(); - if (msg.contains("access denied") || msg.contains("no such repository")) { + if (msg.contains("access denied") + || msg.contains("no such repository") + || msg.contains("Git repository not found")) { createRepository(); } else { repLog.error("Cannot replicate {}; Remote repository error: {}", projectName, msg); @@ -383,7 +376,7 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { } } catch (IOException e) { stateLog.error("Cannot replicate to " + uri, e, getStatesAsArray()); - } catch (RuntimeException | Error e) { + } catch (PermissionBackendException | RuntimeException | Error e) { stateLog.error("Unexpected error during replication to " + uri, e, getStatesAsArray()); } finally { if (git != null) { @@ -430,7 +423,7 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { return target.getName(); } - private void runImpl() throws IOException { + private void runImpl() throws IOException, PermissionBackendException { PushResult res; try (Transport tn = Transport.open(git, uri)) { res = pushVia(tn); @@ -439,7 +432,7 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { } private PushResult pushVia(Transport tn) - throws IOException, NotSupportedException, TransportException { + throws IOException, NotSupportedException, TransportException, PermissionBackendException { tn.applyConfig(config); tn.setCredentialsProvider(credentialsProvider); @@ -457,7 +450,8 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { return tn.push(NullProgressMonitor.INSTANCE, todo); } - private List<RemoteRefUpdate> generateUpdates(Transport tn) throws IOException { + private List<RemoteRefUpdate> generateUpdates(Transport tn) + throws IOException, PermissionBackendException { ProjectControl pc; try { pc = pool.controlFor(projectName); @@ -466,7 +460,14 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { } Map<String, Ref> local = git.getAllRefs(); - if (!pc.allRefsAreVisible()) { + boolean filter; + try { + permissionBackend.user(pc.getUser()).project(projectName).check(ProjectPermission.READ); + filter = false; + } catch (AuthException e) { + filter = true; + } + if (filter) { if (!pushAllRefs) { // If we aren't mirroring, reduce the space we need to filter // to only the references we will update during this operation. @@ -480,16 +481,7 @@ class PushOne implements ProjectRunnable, CanceledWhileRunning { } local = n; } - - try (ReviewDb db = schema.open()) { - local = - new VisibleRefFilter(tagCache, changeNotesFactory, changeCache, git, pc, db, true) - .filter(local, true); - } catch (OrmException e) { - stateLog.error( - "Cannot read database to replicate to " + projectName, e, getStatesAsArray()); - return Collections.emptyList(); - } + local = refFilterFactory.create(pc.getProjectState(), git).filter(local, true); } return pushAllRefs ? doPushAll(tn, local) : doPushDelta(local); diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java index 43d5c5e..c71a792 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java @@ -16,6 +16,7 @@ package com.googlesource.gerrit.plugins.replication; import com.google.gerrit.common.EventDispatcher; import com.google.gerrit.server.events.RefEvent; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult; import java.lang.ref.WeakReference; @@ -43,7 +44,7 @@ public abstract class PushResultProcessing { * * @param message message text. */ - void writeStdOut(final String message) { + void writeStdOut(String message) { // Default doing nothing } @@ -52,7 +53,7 @@ public abstract class PushResultProcessing { * * @param message message text. */ - void writeStdErr(final String message) { + void writeStdErr(String message) { // Default doing nothing } @@ -141,7 +142,7 @@ public abstract class PushResultProcessing { } @Override - void writeStdOut(final String message) { + void writeStdOut(String message) { StartCommand command = sshCommand.get(); if (command != null) { command.writeStdOutSync(message); @@ -149,7 +150,7 @@ public abstract class PushResultProcessing { } @Override - void writeStdErr(final String message) { + void writeStdErr(String message) { StartCommand command = sshCommand.get(); if (command != null) { command.writeStdErrSync(message); @@ -187,7 +188,7 @@ public abstract class PushResultProcessing { private void postEvent(RefEvent event) { try { dispatcher.postEvent(event); - } catch (OrmException e) { + } catch (OrmException | PermissionBackendException e) { log.error("Cannot post event", e); } } diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSiteUser.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSiteUser.java index f3dc04d..91fce7f 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSiteUser.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSiteUser.java @@ -15,7 +15,6 @@ package com.googlesource.gerrit.plugins.replication; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupMembership; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -28,9 +27,7 @@ public class RemoteSiteUser extends CurrentUser { private final GroupMembership effectiveGroups; @Inject - RemoteSiteUser( - CapabilityControl.Factory capabilityControlFactory, @Assisted GroupMembership authGroups) { - super(capabilityControlFactory); + RemoteSiteUser(@Assisted GroupMembership authGroups) { effectiveGroups = authGroups; } 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 57893d8..30aff44 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java @@ -25,7 +25,6 @@ import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; -import com.google.inject.Provider; import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing; import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType; import java.io.File; @@ -35,17 +34,12 @@ import java.net.URISyntaxException; import java.util.Collections; import java.util.HashSet; import java.util.Set; -import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.storage.file.FileRepository; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.RemoteSession; -import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.URIish; -import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.QuotedString; -import org.eclipse.jgit.util.io.StreamCopyThread; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +52,6 @@ public class ReplicationQueue HeadUpdatedListener { static final String REPLICATION_LOG_NAME = "replication_log"; static final Logger repLog = LoggerFactory.getLogger(REPLICATION_LOG_NAME); - private static final int SSH_REMOTE_TIMEOUT = 120 * 1000; private final ReplicationStateListener stateLog; @@ -75,23 +68,26 @@ public class ReplicationQueue } private final WorkQueue workQueue; + private final SshHelper sshHelper; private final DynamicItem<EventDispatcher> dispatcher; private final ReplicationConfig config; - private final Provider<SshSessionFactory> sshSessionFactoryProvider; + private final GerritSshApi gerritAdmin; private volatile boolean running; @Inject ReplicationQueue( WorkQueue wq, + SshHelper sh, + GerritSshApi ga, ReplicationConfig rc, DynamicItem<EventDispatcher> dis, - ReplicationStateListener sl, - Provider<SshSessionFactory> sshSessionFactoryProvider) { + ReplicationStateListener sl) { workQueue = wq; + sshHelper = sh; dispatcher = dis; config = rc; stateLog = sl; - this.sshSessionFactoryProvider = sshSessionFactoryProvider; + gerritAdmin = ga; } @Override @@ -109,8 +105,12 @@ public class ReplicationQueue } } + void scheduleFullSync(Project.NameKey project, String urlMatch, ReplicationState state) { + scheduleFullSync(project, urlMatch, state, false); + } + void scheduleFullSync( - final Project.NameKey project, final String urlMatch, ReplicationState state) { + Project.NameKey project, String urlMatch, ReplicationState state, boolean now) { if (!running) { stateLog.warn("Replication plugin did not finish startup before event", state); return; @@ -119,7 +119,7 @@ public class ReplicationQueue for (Destination cfg : config.getDestinations(FilterType.ALL)) { if (cfg.wouldPushProject(project)) { for (URIish uri : cfg.getURIs(project, urlMatch)) { - cfg.schedule(project, PushOne.ALL_REFS, uri, state); + cfg.schedule(project, PushOne.ALL_REFS, uri, state, now); } } } @@ -146,24 +146,25 @@ public class ReplicationQueue @Override public void onNewProjectCreated(NewProjectCreatedListener.Event event) { - for (URIish uri : - getURIs(new Project.NameKey(event.getProjectName()), FilterType.PROJECT_CREATION)) { - createProject(uri, event.getHeadName()); + Project.NameKey projectName = new Project.NameKey(event.getProjectName()); + for (URIish uri : getURIs(projectName, FilterType.PROJECT_CREATION)) { + createProject(uri, projectName, event.getHeadName()); } } @Override public void onProjectDeleted(ProjectDeletedListener.Event event) { - for (URIish uri : - getURIs(new Project.NameKey(event.getProjectName()), FilterType.PROJECT_DELETION)) { - deleteProject(uri); + Project.NameKey projectName = new Project.NameKey(event.getProjectName()); + for (URIish uri : getURIs(projectName, FilterType.PROJECT_DELETION)) { + deleteProject(uri, projectName); } } @Override public void onHeadUpdated(HeadUpdatedListener.Event event) { - for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), FilterType.ALL)) { - updateHead(uri, event.getNewHeadName()); + Project.NameKey project = new Project.NameKey(event.getProjectName()); + for (URIish uri : getURIs(project, FilterType.ALL)) { + updateHead(uri, project, event.getNewHeadName()); } } @@ -197,18 +198,20 @@ public class ReplicationQueue continue; } - String path = replaceName(uri.getPath(), projectName.get(), config.isSingleProjectMatch()); - if (path == null) { - repLog.warn("adminURL {} does not contain ${name}", uri); - continue; - } + if (!isGerrit(uri)) { + String path = + replaceName(uri.getPath(), projectName.get(), config.isSingleProjectMatch()); + if (path == null) { + repLog.warn("adminURL {} does not contain ${name}", uri); + continue; + } - uri = uri.setPath(path); - if (!isSSH(uri)) { - repLog.warn("adminURL '{}' is invalid: only SSH is supported", uri); - continue; + uri = uri.setPath(path); + if (!isSSH(uri)) { + repLog.warn("adminURL '{}' is invalid: only SSH is supported", uri); + continue; + } } - uris.add(uri); adminURLUsed = true; } @@ -225,13 +228,15 @@ public class ReplicationQueue public boolean createProject(Project.NameKey project, String head) { boolean success = true; for (URIish uri : getURIs(project, FilterType.PROJECT_CREATION)) { - success &= createProject(uri, head); + success &= createProject(uri, project, head); } return success; } - private boolean createProject(URIish replicateURI, String head) { - if (!replicateURI.isRemote()) { + private boolean createProject(URIish replicateURI, Project.NameKey projectName, String head) { + if (isGerrit(replicateURI)) { + gerritAdmin.createProject(replicateURI, projectName, head); + } else if (!replicateURI.isRemote()) { createLocally(replicateURI, head); } else if (isSSH(replicateURI)) { createRemoteSsh(replicateURI, head); @@ -267,9 +272,9 @@ public class ReplicationQueue if (head != null) { cmd = cmd + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(head); } - OutputStream errStream = newErrorBufferStream(); + OutputStream errStream = sshHelper.newErrorBufferStream(); try { - executeRemoteSsh(uri, cmd, errStream); + sshHelper.executeRemoteSsh(uri, cmd, errStream); repLog.info("Created remote repository: {}", uri); } catch (IOException e) { repLog.error( @@ -285,8 +290,11 @@ public class ReplicationQueue } } - private void deleteProject(URIish replicateURI) { - if (!replicateURI.isRemote()) { + private void deleteProject(URIish replicateURI, Project.NameKey projectName) { + if (isGerrit(replicateURI)) { + gerritAdmin.deleteProject(replicateURI, projectName); + repLog.info("Deleted remote repository: " + replicateURI); + } else if (!replicateURI.isRemote()) { deleteLocally(replicateURI); } else if (isSSH(replicateURI)) { deleteRemoteSsh(replicateURI); @@ -329,9 +337,9 @@ public class ReplicationQueue private void deleteRemoteSsh(URIish uri) { String quotedPath = QuotedString.BOURNE.quote(uri.getPath()); String cmd = "rm -rf " + quotedPath; - OutputStream errStream = newErrorBufferStream(); + OutputStream errStream = sshHelper.newErrorBufferStream(); try { - executeRemoteSsh(uri, cmd, errStream); + sshHelper.executeRemoteSsh(uri, cmd, errStream); repLog.info("Deleted remote repository: {}", uri); } catch (IOException e) { repLog.error( @@ -347,8 +355,10 @@ public class ReplicationQueue } } - private void updateHead(URIish replicateURI, String newHead) { - if (!replicateURI.isRemote()) { + private void updateHead(URIish replicateURI, Project.NameKey projectName, String newHead) { + if (isGerrit(replicateURI)) { + gerritAdmin.updateHead(replicateURI, projectName, newHead); + } else if (!replicateURI.isRemote()) { updateHeadLocally(replicateURI, newHead); } else if (isSSH(replicateURI)) { updateHeadRemoteSsh(replicateURI, newHead); @@ -365,9 +375,9 @@ public class ReplicationQueue String quotedPath = QuotedString.BOURNE.quote(uri.getPath()); String cmd = "cd " + quotedPath + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(newHead); - OutputStream errStream = newErrorBufferStream(); + OutputStream errStream = sshHelper.newErrorBufferStream(); try { - executeRemoteSsh(uri, cmd, errStream); + sshHelper.executeRemoteSsh(uri, cmd, errStream); } catch (IOException e) { repLog.error( "Error updating HEAD of remote repository at {} to {}:\n" @@ -394,57 +404,6 @@ public class ReplicationQueue } } - private void executeRemoteSsh(URIish uri, String cmd, OutputStream errStream) throws IOException { - RemoteSession ssh = connect(uri); - Process proc = ssh.exec(cmd, 0); - proc.getOutputStream().close(); - StreamCopyThread out = new StreamCopyThread(proc.getInputStream(), errStream); - StreamCopyThread err = new StreamCopyThread(proc.getErrorStream(), errStream); - out.start(); - err.start(); - try { - proc.waitFor(); - out.halt(); - err.halt(); - } catch (InterruptedException interrupted) { - // Don't wait, drop out immediately. - } - ssh.disconnect(); - } - - private RemoteSession connect(URIish uri) throws TransportException { - return sshSessionFactoryProvider.get().getSession(uri, null, FS.DETECTED, SSH_REMOTE_TIMEOUT); - } - - private static OutputStream newErrorBufferStream() { - return new OutputStream() { - private final StringBuilder out = new StringBuilder(); - private final StringBuilder line = new StringBuilder(); - - @Override - public synchronized String toString() { - while (out.length() > 0 && out.charAt(out.length() - 1) == '\n') { - out.setLength(out.length() - 1); - } - return out.toString(); - } - - @Override - public synchronized void write(final int b) { - if (b == '\r') { - return; - } - - line.append((char) b); - - if (b == '\n') { - out.append(line); - line.setLength(0); - } - } - }; - } - private static boolean isSSH(URIish uri) { String scheme = uri.getScheme(); if (!uri.isRemote()) { @@ -458,4 +417,9 @@ public class ReplicationQueue } return false; } + + private static boolean isGerrit(URIish uri) { + String scheme = uri.getScheme(); + return scheme != null && scheme.toLowerCase().equals("gerrit+ssh"); + } } diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java index 9a68c83..86557e2 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java @@ -150,11 +150,11 @@ public class ReplicationState { allPushTasksFinished.await(); } - public void writeStdOut(final String message) { + public void writeStdOut(String message) { pushResultProcessing.writeStdOut(message); } - public void writeStdErr(final String message) { + public void writeStdErr(String message) { pushResultProcessing.writeStdErr(message); } diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/SshHelper.java b/src/main/java/com/googlesource/gerrit/plugins/replication/SshHelper.java new file mode 100644 index 0000000..68e9652 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/SshHelper.java @@ -0,0 +1,89 @@ +// Copyright (C) 2017 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.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.io.OutputStream; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.transport.RemoteSession; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.io.StreamCopyThread; + +class SshHelper { + private static final int SSH_REMOTE_TIMEOUT = 120 * 1000; // 2 minutes = 120 * 1000ms + + private final Provider<SshSessionFactory> sshSessionFactoryProvider; + + @Inject + SshHelper(Provider<SshSessionFactory> sshSessionFactoryProvider) { + this.sshSessionFactoryProvider = sshSessionFactoryProvider; + } + + int executeRemoteSsh(URIish uri, String cmd, OutputStream errStream) throws IOException { + RemoteSession ssh = connect(uri); + Process proc = ssh.exec(cmd, 0); + proc.getOutputStream().close(); + StreamCopyThread out = new StreamCopyThread(proc.getInputStream(), errStream); + StreamCopyThread err = new StreamCopyThread(proc.getErrorStream(), errStream); + out.start(); + err.start(); + try { + proc.waitFor(); + out.halt(); + err.halt(); + } catch (InterruptedException interrupted) { + // Don't wait, drop out immediately. + } + ssh.disconnect(); + return proc.exitValue(); + } + + OutputStream newErrorBufferStream() { + return new OutputStream() { + private final StringBuilder out = new StringBuilder(); + private final StringBuilder line = new StringBuilder(); + + @Override + public synchronized String toString() { + while (out.length() > 0 && out.charAt(out.length() - 1) == '\n') { + out.setLength(out.length() - 1); + } + return out.toString(); + } + + @Override + public synchronized void write(int b) { + if (b == '\r') { + return; + } + + line.append((char) b); + + if (b == '\n') { + out.append(line); + line.setLength(0); + } + } + }; + } + + RemoteSession connect(URIish uri) throws TransportException { + return sshSessionFactoryProvider.get().getSession(uri, null, FS.DETECTED, SSH_REMOTE_TIMEOUT); + } +} 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 9336466..5aa0861 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java @@ -43,6 +43,9 @@ final class StartCommand extends SshCommand { @Option(name = "--wait", usage = "wait for replication to finish before exiting") private boolean wait; + @Option(name = "--now", usage = "start replication without waiting for replicationDelay") + private boolean now; + @Argument(index = 0, multiValued = true, metaVar = "PATTERN", usage = "project name pattern") private List<String> projectPatterns = new ArrayList<>(2); @@ -65,7 +68,7 @@ final class StartCommand extends SshCommand { projectFilter = new ReplicationFilter(projectPatterns); } - future = pushFactory.create(urlMatch, projectFilter, state).schedule(0, TimeUnit.SECONDS); + future = pushFactory.create(urlMatch, projectFilter, state, now).schedule(0, TimeUnit.SECONDS); if (wait) { if (future != null) { @@ -93,7 +96,7 @@ final class StartCommand extends SshCommand { } } - public void writeStdOutSync(final String message) { + public void writeStdOutSync(String message) { if (wait) { synchronized (stdout) { stdout.println(message); @@ -102,7 +105,7 @@ final class StartCommand extends SshCommand { } } - public void writeStdErrSync(final String message) { + public void writeStdErrSync(String message) { if (wait) { synchronized (stderr) { stderr.println(message); diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md index dd9cea5..69a371b 100644 --- a/src/main/resources/Documentation/about.md +++ b/src/main/resources/Documentation/about.md @@ -16,3 +16,15 @@ configuration is not recommended. It is also possible to specify a local path as replication target. This makes e.g. sense if a network share is mounted to which the repositories should be replicated. +Replication of account data (NoteDb) +------------------------------------ + +To replicate the account data in NoteDb the following branches from the +`All-Users` repository must be replicated: + +* `refs/users/*` (user branches) +* `refs/meta/external-ids` (external IDs) +* `refs/starred-changes/*` (star labels) +* `refs/sequences/accounts` (account sequence numbers, not needed for Gerrit + slaves) + diff --git a/src/main/resources/Documentation/cmd-start.md b/src/main/resources/Documentation/cmd-start.md index 59c3d1d..6af73af 100644 --- a/src/main/resources/Documentation/cmd-start.md +++ b/src/main/resources/Documentation/cmd-start.md @@ -9,6 +9,7 @@ SYNOPSIS -------- ``` ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start + [--now] [--wait] [--url <PATTERN>] {--all | <PROJECT PATTERN> ...} @@ -85,6 +86,10 @@ This command is intended to be used in scripts. OPTIONS ------- +`--now` +: Start replicating right away without waiting the per remote + replication delay. + `--wait` : Wait for replication to finish before exiting. diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 099608d..c066513 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md @@ -153,6 +153,18 @@ remote.NAME.adminUrl local environment. In that case, an alternative SSH url could be specified to repository creation. + To enable replication to different Gerrit instance use `gerrit+ssh://` + as protocol name followed by hostname of another Gerrit server eg. + + `gerrit+ssh://replica1.my.org/` + + In this case replication will use Gerrit's SSH API to + create/remove projects and update repository HEAD references. + + NOTE: In order to replicate project deletion, the + link:https://gerrit-review.googlesource.com/admin/projects/plugins/delete-project delete-project[delete-project] + plugin must be installed on the other Gerrit. + remote.NAME.receivepack : Path of the `git-receive-pack` executable on the remote system, if using the SSH transport. diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java index 41829bc..337bd1d 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java @@ -24,6 +24,7 @@ import static org.easymock.EasyMock.verify; import com.google.gerrit.common.EventDispatcher; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.client.KeyUtil; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; @@ -58,7 +59,8 @@ public class GitUpdateProcessingTest { } @Test - public void headRefReplicated() throws URISyntaxException, OrmException { + public void headRefReplicated() + throws URISyntaxException, OrmException, PermissionBackendException { reset(dispatcherMock); RefReplicatedEvent expectedEvent = new RefReplicatedEvent( @@ -81,7 +83,8 @@ public class GitUpdateProcessingTest { } @Test - public void changeRefReplicated() throws URISyntaxException, OrmException { + public void changeRefReplicated() + throws URISyntaxException, OrmException, PermissionBackendException { reset(dispatcherMock); RefReplicatedEvent expectedEvent = new RefReplicatedEvent( @@ -104,7 +107,7 @@ public class GitUpdateProcessingTest { } @Test - public void onAllNodesReplicated() throws OrmException { + public void onAllNodesReplicated() throws OrmException, PermissionBackendException { reset(dispatcherMock); RefReplicationDoneEvent expectedDoneEvent = new RefReplicationDoneEvent("someProject", "refs/heads/master", 5); |