Merge branch 'stable-2.13' into stable-2.14

* stable-2.13:
  Fix minor nits in documentation of replication.maxRetries
  Fix replication retries when maxRetries is set to 0
  Set max retries to avoid queue congestion

Change-Id: Icf972f3df7d3d59925db670bc1f033028306d0c1
diff --git a/.gitignore b/.gitignore
index 9c143f3..1e8377d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,4 @@
 /.settings/org.eclipse.m2e.core.prefs
 /.idea
 replication.iml
-/.buckd
-/buck-cache
-/buck-out
+*.iml
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 17904c0..1792fcc 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,9 +1,9 @@
 #Fri Jul 16 23:39:13 PDT 2010
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
 org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.compiler.source=1.8
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git a/BUCK b/BUCK
deleted file mode 100644
index d658b92..0000000
--- a/BUCK
+++ /dev/null
@@ -1,40 +0,0 @@
-include_defs('//lib/maven.defs')
-
-gerrit_plugin(
-  name = 'replication',
-  srcs = glob(['src/main/java/**/*.java']),
-  resources = glob(['src/main/resources/**/*']),
-  manifest_entries = [
-    'Implementation-Title: Replication plugin',
-    'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/replication',
-    'Gerrit-PluginName: replication',
-    'Gerrit-Module: com.googlesource.gerrit.plugins.replication.ReplicationModule',
-    'Gerrit-SshModule: com.googlesource.gerrit.plugins.replication.SshModule'
-  ],
-  deps = [
-    ':commons-io',
-  ],
-  provided_deps = [
-    '//lib:gson',
-    '//lib/log:log4j'
-  ],
-)
-
-maven_jar(
-  name = 'commons-io',
-  id = 'commons-io:commons-io:1.4',
-  sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
-  license = 'Apache2.0',
-)
-
-java_test(
-  name = 'replication_tests',
-  srcs = glob(['src/test/java/**/*.java']),
-  labels = ['replication'],
-  source_under_test = [':replication__plugin'],
-  deps = [
-    ':replication__plugin',
-    '//gerrit-acceptance-framework:lib',
-    '//gerrit-plugin-api:lib',
-  ],
-)
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..1cad80c
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,46 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+
+gerrit_plugin(
+    name = "replication",
+    srcs = glob(["src/main/java/**/*.java"]),
+    manifest_entries = [
+        "Implementation-Title: Replication plugin",
+        "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/replication",
+        "Gerrit-PluginName: replication",
+        "Gerrit-InitStep: com.googlesource.gerrit.plugins.replication.Init",
+        "Gerrit-Module: com.googlesource.gerrit.plugins.replication.ReplicationModule",
+        "Gerrit-SshModule: com.googlesource.gerrit.plugins.replication.SshModule",
+    ],
+    resources = glob(["src/main/resources/**/*"]),
+    deps = [
+        "//lib:commons-io",
+    ],
+)
+
+junit_tests(
+    name = "replication_tests",
+    srcs = glob(["src/test/java/**/*Test.java"]),
+    tags = ["replication"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":replication__plugin",
+        ":replication_util",
+        "//gerrit-acceptance-framework:lib",
+        "//gerrit-plugin-api:lib",
+    ],
+)
+
+java_library(
+    name = "replication_util",
+    testonly = 1,
+    srcs = glob(
+        ["src/test/java/**/*.java"],
+        exclude = ["src/test/java/**/*Test.java"],
+    ),
+    deps = [
+        ":replication__plugin",
+        "//gerrit-acceptance-framework:lib",
+        "//gerrit-plugin-api:lib",
+    ],
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
index 22b29b1..55c7072 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
@@ -14,73 +14,43 @@
 package com.googlesource.gerrit.plugins.replication;
 
 import com.google.gerrit.common.FileUtil;
-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.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
 import com.google.inject.Singleton;
-
+import java.io.IOException;
+import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.util.List;
-
 @Singleton
 public class AutoReloadConfigDecorator implements ReplicationConfig {
-  private static final Logger log = LoggerFactory
-      .getLogger(AutoReloadConfigDecorator.class);
+  private static final Logger log = LoggerFactory.getLogger(AutoReloadConfigDecorator.class);
   private ReplicationFileBasedConfig currentConfig;
   private long currentConfigTs;
 
-  private final Injector injector;
   private final SitePaths site;
-  private final RemoteSiteUser.Factory remoteSiteUserFactory;
-  private final PluginUser pluginUser;
-  private final GitRepositoryManager gitRepositoryManager;
-  private final GroupBackend groupBackend;
   private final WorkQueue workQueue;
-  private final ReplicationStateListener stateLog;
-  private final GroupIncludeCache groupIncludeCache;
+  private final DestinationFactory destinationFactory;
 
   @Inject
-  public AutoReloadConfigDecorator(Injector injector,
-      SitePaths site,
-      RemoteSiteUser.Factory ruf,
-      PluginUser pu,
-      GitRepositoryManager grm,
-      GroupBackend gb,
-      WorkQueue workQueue,
-      ReplicationStateListener stateLog,
-      GroupIncludeCache groupIncludeCache)
+  public AutoReloadConfigDecorator(
+      SitePaths site, WorkQueue workQueue, DestinationFactory destinationFactory)
       throws ConfigInvalidException, IOException {
-    this.injector = injector;
     this.site = site;
-    this.remoteSiteUserFactory = ruf;
-    this.pluginUser = pu;
-    this.gitRepositoryManager = grm;
-    this.groupBackend = gb;
-    this.groupIncludeCache = groupIncludeCache;
+    this.destinationFactory = destinationFactory;
     this.currentConfig = loadConfig();
     this.currentConfigTs = getLastModified(currentConfig);
     this.workQueue = workQueue;
-    this.stateLog = stateLog;
   }
 
   private static long getLastModified(ReplicationFileBasedConfig cfg) {
     return FileUtil.lastModified(cfg.getCfgPath());
   }
 
-  private ReplicationFileBasedConfig loadConfig()
-      throws ConfigInvalidException, IOException {
-    return new ReplicationFileBasedConfig(injector, site, remoteSiteUserFactory,
-        pluginUser, gitRepositoryManager, groupBackend, stateLog,
-        groupIncludeCache);
+  private ReplicationFileBasedConfig loadConfig() throws ConfigInvalidException, IOException {
+    return new ReplicationFileBasedConfig(site, destinationFactory);
   }
 
   private synchronized boolean isAutoReload() {
@@ -104,16 +74,16 @@
 
           this.currentConfig = newConfig;
           this.currentConfigTs = lastModified;
-          log.info("Configuration reloaded: "
-            + currentConfig.getDestinations(FilterType.ALL).size() + " destinations, "
-            + discarded + " replication events discarded");
-
+          log.info(
+              "Configuration reloaded: "
+                  + currentConfig.getDestinations(FilterType.ALL).size()
+                  + " destinations, "
+                  + discarded
+                  + " replication events discarded");
         }
       }
     } catch (Exception e) {
-      log.error(
-          "Cannot reload replication configuration: keeping existing settings",
-          e);
+      log.error("Cannot reload replication configuration: keeping existing settings", e);
       return;
     }
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
index 3a0cc3f..f8737b6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
@@ -18,19 +18,16 @@
 
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
-
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.util.concurrent.atomic.AtomicReference;
-
-public class AutoReloadSecureCredentialsFactoryDecorator implements
-    CredentialsFactory {
-  private static final Logger log = LoggerFactory
-      .getLogger(AutoReloadSecureCredentialsFactoryDecorator.class);
+public class AutoReloadSecureCredentialsFactoryDecorator implements CredentialsFactory {
+  private static final Logger log =
+      LoggerFactory.getLogger(AutoReloadSecureCredentialsFactoryDecorator.class);
 
   private final AtomicReference<SecureCredentialsFactory> secureCredentialsFactory;
   private volatile long secureCredentialsFactoryLoadTs;
@@ -38,13 +35,12 @@
   private ReplicationFileBasedConfig config;
 
   @Inject
-  public AutoReloadSecureCredentialsFactoryDecorator(SitePaths site,
-      ReplicationFileBasedConfig config) throws ConfigInvalidException,
-      IOException {
+  public AutoReloadSecureCredentialsFactoryDecorator(
+      SitePaths site, ReplicationFileBasedConfig config)
+      throws ConfigInvalidException, IOException {
     this.site = site;
     this.config = config;
-    this.secureCredentialsFactory =
-        new AtomicReference<>(new SecureCredentialsFactory(site));
+    this.secureCredentialsFactory = new AtomicReference<>(new SecureCredentialsFactory(site));
     this.secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
   }
 
@@ -59,21 +55,23 @@
   public SecureCredentialsProvider create(String remoteName) {
     try {
       if (needsReload()) {
-        secureCredentialsFactory.compareAndSet(secureCredentialsFactory.get(),
-            new SecureCredentialsFactory(site));
+        secureCredentialsFactory.compareAndSet(
+            secureCredentialsFactory.get(), new SecureCredentialsFactory(site));
         secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
         log.info("secure.config reloaded as it was updated on the file system");
       }
     } catch (Exception e) {
-      log.error("Unexpected error while trying to reload "
-          + "secure.config: keeping existing credentials", e);
+      log.error(
+          "Unexpected error while trying to reload "
+              + "secure.config: keeping existing credentials",
+          e);
     }
 
     return secureCredentialsFactory.get().create(remoteName);
   }
 
   private boolean needsReload() {
-    return config.getConfig().getBoolean("gerrit", "autoReload", false) &&
-        getSecureConfigLastEditTs() != secureCredentialsFactoryLoadTs;
+    return config.getConfig().getBoolean("gerrit", "autoReload", false)
+        && getSecureConfigLastEditTs() != secureCredentialsFactoryLoadTs;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
index e960533..10719c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
@@ -16,5 +16,4 @@
 public interface CredentialsFactory {
 
   SecureCredentialsProvider create(String remoteName);
-
 }
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 ed361a9..f8e2d7b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -14,15 +14,20 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
+import static com.googlesource.gerrit.plugins.replication.PushResultProcessing.resolveNodeName;
+
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 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.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -45,16 +50,6 @@
 import com.google.inject.Provides;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.servlet.RequestScoped;
-
-import org.apache.commons.io.FilenameUtils;
-import org.eclipse.jgit.lib.Constants;
-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.URIish;
-import org.slf4j.Logger;
-
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
@@ -63,6 +58,14 @@
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
+import org.apache.commons.io.FilenameUtils;
+import org.eclipse.jgit.lib.Constants;
+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.URIish;
+import org.slf4j.Logger;
 
 public class Destination {
   private static final Logger repLog = ReplicationQueue.repLog;
@@ -76,31 +79,36 @@
   private volatile WorkQueue.Executor pool;
   private final PerThreadRequestScope.Scoper threadScoper;
   private final DestinationConfiguration config;
+  private final DynamicItem<EventDispatcher> eventDispatcher;
 
   protected enum RetryReason {
-    TRANSPORT_ERROR, COLLISION, REPOSITORY_MISSING;
+    TRANSPORT_ERROR,
+    COLLISION,
+    REPOSITORY_MISSING;
   }
 
   public static class QueueInfo {
     public final Map<URIish, PushOne> pending;
     public final Map<URIish, PushOne> inFlight;
 
-    public QueueInfo(Map<URIish, PushOne> pending,
-        Map<URIish, PushOne> inFlight) {
+    public QueueInfo(Map<URIish, PushOne> pending, Map<URIish, PushOne> inFlight) {
       this.pending = ImmutableMap.copyOf(pending);
       this.inFlight = ImmutableMap.copyOf(inFlight);
     }
   }
 
-  protected Destination(Injector injector,
+  protected Destination(
+      Injector injector,
       DestinationConfiguration cfg,
       RemoteSiteUser.Factory replicationUserFactory,
       PluginUser pluginUser,
       GitRepositoryManager gitRepositoryManager,
       GroupBackend groupBackend,
       ReplicationStateListener stateLog,
-      GroupIncludeCache groupIncludeCache) {
+      GroupIncludeCache groupIncludeCache,
+      DynamicItem<EventDispatcher> eventDispatcher) {
     config = cfg;
+    this.eventDispatcher = eventDispatcher;
     gitManager = gitRepositoryManager;
     this.stateLog = stateLog;
 
@@ -113,59 +121,62 @@
           builder.add(g.getUUID());
           addRecursiveParents(g.getUUID(), builder, groupIncludeCache);
         } else {
-          repLog.warn(String.format(
-              "Group \"%s\" not recognized, removing from authGroup", name));
+          repLog.warn(String.format("Group \"%s\" not recognized, removing from authGroup", name));
         }
       }
-      remoteUser = replicationUserFactory.create(
-          new ListGroupMembership(builder.build()));
+      remoteUser = replicationUserFactory.create(new ListGroupMembership(builder.build()));
     } else {
       remoteUser = pluginUser;
     }
 
-    Injector child = injector.createChildInjector(new FactoryModule() {
-      @Override
-      protected void configure() {
-        bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
-        bind(PerThreadRequestScope.Propagator.class);
-        bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
+    Injector child =
+        injector.createChildInjector(
+            new FactoryModule() {
+              @Override
+              protected void configure() {
+                bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
+                bind(PerThreadRequestScope.Propagator.class);
+                bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
 
-        bind(Destination.class).toInstance(Destination.this);
-        bind(RemoteConfig.class).toInstance(config.getRemoteConfig());
-        install(new FactoryModuleBuilder().build(PushOne.Factory.class));
-      }
+                bind(Destination.class).toInstance(Destination.this);
+                bind(RemoteConfig.class).toInstance(config.getRemoteConfig());
+                install(new FactoryModuleBuilder().build(PushOne.Factory.class));
+              }
 
-      @Provides
-      public PerThreadRequestScope.Scoper provideScoper(
-          final PerThreadRequestScope.Propagator propagator,
-          final Provider<RequestScopedReviewDbProvider> dbProvider) {
-        final RequestContext requestContext = new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return remoteUser;
-          }
+              @Provides
+              public PerThreadRequestScope.Scoper provideScoper(
+                  final PerThreadRequestScope.Propagator propagator,
+                  final Provider<RequestScopedReviewDbProvider> dbProvider) {
+                final RequestContext requestContext =
+                    new RequestContext() {
+                      @Override
+                      public CurrentUser getUser() {
+                        return remoteUser;
+                      }
 
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return dbProvider.get();
-          }
-        };
-        return new PerThreadRequestScope.Scoper() {
-          @Override
-          public <T> Callable<T> scope(Callable<T> callable) {
-            return propagator.scope(requestContext, callable);
-          }
-        };
-      }
-    });
+                      @Override
+                      public Provider<ReviewDb> getReviewDbProvider() {
+                        return dbProvider.get();
+                      }
+                    };
+                return new PerThreadRequestScope.Scoper() {
+                  @Override
+                  public <T> Callable<T> scope(Callable<T> callable) {
+                    return propagator.scope(requestContext, callable);
+                  }
+                };
+              }
+            });
 
     projectControlFactory = child.getInstance(ProjectControl.Factory.class);
     opFactory = child.getInstance(PushOne.Factory.class);
     threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
   }
 
-  private void addRecursiveParents(AccountGroup.UUID g,
-      Builder<AccountGroup.UUID> builder, GroupIncludeCache groupIncludeCache) {
+  private void addRecursiveParents(
+      AccountGroup.UUID g,
+      Builder<AccountGroup.UUID> builder,
+      GroupIncludeCache groupIncludeCache) {
     for (AccountGroup.UUID p : groupIncludeCache.parentGroupsOf(g)) {
       if (builder.build().contains(p)) {
         continue;
@@ -200,52 +211,55 @@
   }
 
   private boolean shouldReplicate(ProjectControl projectControl) {
-    return projectControl.isReadable() && (!projectControl.isHidden()
-        || config.replicateHiddenProjects());
+    return projectControl.isReadable()
+        && (!projectControl.isHidden() || config.replicateHiddenProjects());
   }
 
-  private boolean shouldReplicate(final Project.NameKey project, final String ref,
-      ReplicationState... states) {
+  private boolean shouldReplicate(
+      final Project.NameKey project, final String ref, ReplicationState... states) {
     try {
-      return threadScoper.scope(new Callable<Boolean>() {
-        @Override
-        public Boolean call() throws NoSuchProjectException {
-          ProjectControl projectControl = controlFor(project);
-          return shouldReplicate(projectControl)
-              && (PushOne.ALL_REFS.equals(ref)
-                  || projectControl.controlForRef(ref).isVisible());
-        }
-      }).call();
+      return threadScoper
+          .scope(
+              new Callable<Boolean>() {
+                @Override
+                public Boolean call() throws NoSuchProjectException {
+                  ProjectControl projectControl = controlFor(project);
+                  return shouldReplicate(projectControl)
+                      && (PushOne.ALL_REFS.equals(ref)
+                          || projectControl.controlForRef(ref).isVisible());
+                }
+              })
+          .call();
     } catch (NoSuchProjectException err) {
-      stateLog.error(String.format("source project %s not available", project),
-          err, states);
+      stateLog.error(String.format("source project %s not available", project), err, states);
     } catch (Exception e) {
-      throw Throwables.propagate(e);
-    }
-    return false;
-  }
-
-  private boolean shouldReplicate(final Project.NameKey project,
-      ReplicationState... states) {
-    try {
-      return threadScoper.scope(new Callable<Boolean>() {
-        @Override
-        public Boolean call() throws NoSuchProjectException {
-          return shouldReplicate(controlFor(project));
-        }
-      }).call();
-    } catch (NoSuchProjectException err) {
-      stateLog.error(String.format("source project %s not available", project),
-          err, states);
-    } catch (Exception e) {
-      Throwables.propagateIfPossible(e);
+      Throwables.throwIfUnchecked(e);
       throw new RuntimeException(e);
     }
     return false;
   }
 
-  void schedule(Project.NameKey project, String ref, URIish uri,
-      ReplicationState state) {
+  private boolean shouldReplicate(final Project.NameKey project, ReplicationState... states) {
+    try {
+      return threadScoper
+          .scope(
+              new Callable<Boolean>() {
+                @Override
+                public Boolean call() throws NoSuchProjectException {
+                  return shouldReplicate(controlFor(project));
+                }
+              })
+          .call();
+    } catch (NoSuchProjectException err) {
+      stateLog.error(String.format("source project %s not available", project), err, states);
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      throw new RuntimeException(e);
+    }
+    return false;
+  }
+
+  void schedule(Project.NameKey project, String ref, URIish uri, ReplicationState state) {
     repLog.info("scheduling replication {}:{} => {}", project, ref, uri);
     if (!shouldReplicate(project, ref, state)) {
       return;
@@ -266,13 +280,11 @@
               return;
             }
           } catch (IOException err) {
-            stateLog.error(String.format(
-                "cannot check type of project %s", project), err, state);
+            stateLog.error(String.format("cannot check type of project %s", project), err, state);
             return;
           }
         } catch (IOException err) {
-          stateLog.error(String.format(
-              "source project %s not available", project), err, state);
+          stateLog.error(String.format("source project %s not available", project), err, state);
           return;
         }
       }
@@ -282,14 +294,16 @@
       PushOne e = pending.get(uri);
       if (e == null) {
         e = opFactory.create(project, uri);
+        addRef(e, ref);
+        e.addState(ref, state);
         pool.schedule(e, config.getDelay(), TimeUnit.SECONDS);
         pending.put(uri, e);
+      } else if (!e.getRefs().contains(ref)) {
+        addRef(e, ref);
+        e.addState(ref, state);
       }
-      e.addRef(ref);
       state.increasePushTaskCount(project.get(), ref);
-      e.addState(ref, state);
-      repLog.info("scheduled {}:{} => {} to run after {}s", project, ref,
-          e, config.getDelay());
+      repLog.info("scheduled {}:{} => {} to run after {}s", project, ref, e, config.getDelay());
     }
   }
 
@@ -300,32 +314,29 @@
     }
   }
 
+  private void addRef(PushOne e, String ref) {
+    e.addRef(ref);
+    postEvent(e, ref);
+  }
+
   /**
    * It schedules again a PushOp instance.
-   * <p>
-   * If the reason for rescheduling is to avoid a collision
-   * with an in-flight push to the same URI, we don't
-   * mark the operation as "retrying," and we schedule
-   * using the replication delay, rather than the retry
-   * delay.  Otherwise,  the operation is marked as
-   * "retrying" and scheduled to run following the
-   * minutes count determined by class attribute retryDelay.
-   * <p>
-   * In case the PushOp instance to be scheduled has same
-   * URI than one marked as "retrying," it adds to the one
-   * pending the refs list of the parameter instance.
-   * <p>
-   * In case the PushOp instance to be scheduled has the
-   * same URI as one pending, but not marked "retrying," it
-   * indicates the one pending should be canceled when it
-   * starts executing, removes it from pending list, and
-   * adds its refs to the parameter instance. The parameter
-   * instance is scheduled for retry.
-   * <p>
-   * Notice all operations to indicate a PushOp should be
-   * canceled, or it is retrying, or remove/add it from/to
-   * pending Map should be protected by synchronizing on the
-   * stateLock object.
+   *
+   * <p>If the reason for rescheduling is to avoid a collision with an in-flight push to the same
+   * URI, we don't mark the operation as "retrying," and we schedule using the replication delay,
+   * rather than the retry delay. Otherwise, the operation is marked as "retrying" and scheduled to
+   * run following the minutes count determined by class attribute retryDelay.
+   *
+   * <p>In case the PushOp instance to be scheduled has same URI than one marked as "retrying," it
+   * adds to the one pending the refs list of the parameter instance.
+   *
+   * <p>In case the PushOp instance to be scheduled has the same URI as one pending, but not marked
+   * "retrying," it indicates the one pending should be canceled when it starts executing, removes
+   * it from pending list, and adds its refs to the parameter instance. The parameter instance is
+   * scheduled for retry.
+   *
+   * <p>Notice all operations to indicate a PushOp should be canceled, or it is retrying, or
+   * remove/add it from/to pending Map should be protected by synchronizing on the stateLock object.
    *
    * @param pushOp The PushOp instance to be scheduled.
    */
@@ -378,13 +389,19 @@
         pending.put(uri, pushOp);
         switch (reason) {
           case COLLISION:
-            pool.schedule(pushOp, config.getDelay(), TimeUnit.SECONDS);
+            pool.schedule(pushOp, config.getRescheduleDelay(), TimeUnit.SECONDS);
             break;
           case TRANSPORT_ERROR:
           case REPOSITORY_MISSING:
           default:
             if (pushOp.setToRetry()) {
               pool.schedule(pushOp, config.getRetryDelay(), TimeUnit.MINUTES);
+            } else {
+              pushOp.canceledByReplication();
+              pending.remove(uri);
+              stateLog.error(
+                  "Push to " + pushOp.getURI() + " cancelled after maximum number of retries",
+                  pushOp.getStatesAsArray());
             }
             break;
         }
@@ -392,8 +409,7 @@
     }
   }
 
-  ProjectControl controlFor(Project.NameKey project)
-      throws NoSuchProjectException {
+  ProjectControl controlFor(Project.NameKey project) throws NoSuchProjectException {
     return projectControlFactory.controlFor(project);
   }
 
@@ -474,8 +490,7 @@
   }
 
   List<URIish> getURIs(Project.NameKey project, String urlMatch) {
-    List<URIish> r = Lists.newArrayListWithCapacity(
-        config.getRemoteConfig().getURIs().size());
+    List<URIish> r = Lists.newArrayListWithCapacity(config.getRemoteConfig().getURIs().size());
     for (URIish uri : config.getRemoteConfig().getURIs()) {
       if (matches(uri, urlMatch)) {
         String name = project.get();
@@ -490,12 +505,11 @@
         } else if (remoteNameStyle.equals("basenameOnly")) {
           name = FilenameUtils.getBaseName(name);
         } else if (!remoteNameStyle.equals("slash")) {
-          repLog.debug(String.format(
-              "Unknown remoteNameStyle: %s, falling back to slash",
-              remoteNameStyle));
+          repLog.debug(
+              String.format("Unknown remoteNameStyle: %s, falling back to slash", remoteNameStyle));
         }
-        String replacedPath = ReplicationQueue.replaceName(uri.getPath(), name,
-            isSingleProjectMatch());
+        String replacedPath =
+            ReplicationQueue.replaceName(uri.getPath(), name, isSingleProjectMatch());
         if (replacedPath != null) {
           uri = uri.setPath(replacedPath);
           r.add(uri);
@@ -507,8 +521,8 @@
 
   static boolean needsUrlEncoding(URIish uri) {
     return "http".equalsIgnoreCase(uri.getScheme())
-      || "https".equalsIgnoreCase(uri.getScheme())
-      || "amazon-s3".equalsIgnoreCase(uri.getScheme());
+        || "https".equalsIgnoreCase(uri.getScheme())
+        || "amazon-s3".equalsIgnoreCase(uri.getScheme());
   }
 
   static String encode(String str) {
@@ -518,9 +532,7 @@
       // path used to the repository. Space is incorrectly encoded as '+' for this
       // context. In the path part of a URI space should be %20, but in form data
       // space is '+'. Our cleanup replace fixes these two issues.
-      return URLEncoder.encode(str, "UTF-8")
-        .replaceAll("%2[fF]", "/")
-        .replace("+", "%20");
+      return URLEncoder.encode(str, "UTF-8").replaceAll("%2[fF]", "/").replace("+", "%20");
     } catch (UnsupportedEncodingException e) {
       throw new RuntimeException(e);
     }
@@ -560,4 +572,11 @@
     }
     return uri.toString().contains(urlMatch);
   }
+
+  private void postEvent(PushOne pushOp, String ref) {
+    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);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
index f79f616..856ffb1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
@@ -16,12 +16,15 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.RemoteConfig;
 
 class DestinationConfiguration {
+  static final int DEFAULT_REPLICATION_DELAY = 15;
+  static final int DEFAULT_RESCHEDULE_DELAY = 3;
+
   private final int delay;
+  private final int rescheduleDelay;
   private final int retryDelay;
   private final int lockErrorMaxRetries;
   private final ImmutableList<String> adminUrls;
@@ -40,29 +43,23 @@
   DestinationConfiguration(RemoteConfig remoteConfig, Config cfg) {
     this.remoteConfig = remoteConfig;
     String name = remoteConfig.getName();
-    urls = ImmutableList.copyOf(
-        cfg.getStringList("remote", name, "url"));
-    delay = Math.max(0, getInt(remoteConfig, cfg, "replicationdelay", 15));
-    projects = ImmutableList.copyOf(
-        cfg.getStringList("remote", name, "projects"));
-    adminUrls = ImmutableList.copyOf(
-        cfg.getStringList("remote", name, "adminUrl"));
+    urls = ImmutableList.copyOf(cfg.getStringList("remote", name, "url"));
+    delay = Math.max(0, getInt(remoteConfig, cfg, "replicationdelay", DEFAULT_REPLICATION_DELAY));
+    rescheduleDelay =
+        Math.max(3, getInt(remoteConfig, cfg, "rescheduledelay", DEFAULT_RESCHEDULE_DELAY));
+    projects = ImmutableList.copyOf(cfg.getStringList("remote", name, "projects"));
+    adminUrls = ImmutableList.copyOf(cfg.getStringList("remote", name, "adminUrl"));
     retryDelay = Math.max(0, getInt(remoteConfig, cfg, "replicationretry", 1));
     poolThreads = Math.max(0, getInt(remoteConfig, cfg, "threads", 1));
-    authGroupNames = ImmutableList.copyOf(
-        cfg.getStringList("remote", name, "authGroup"));
+    authGroupNames = ImmutableList.copyOf(cfg.getStringList("remote", name, "authGroup"));
     lockErrorMaxRetries = cfg.getInt("replication", "lockErrorMaxRetries", 0);
 
-    createMissingRepos =
-        cfg.getBoolean("remote", name, "createMissingRepositories", true);
-    replicatePermissions =
-        cfg.getBoolean("remote", name, "replicatePermissions", true);
-    replicateProjectDeletions =
-        cfg.getBoolean("remote", name, "replicateProjectDeletions", false);
-    replicateHiddenProjects =
-        cfg.getBoolean("remote", name, "replicateHiddenProjects", false);
-    remoteNameStyle = MoreObjects.firstNonNull(
-        cfg.getString("remote", name, "remoteNameStyle"), "slash");
+    createMissingRepos = cfg.getBoolean("remote", name, "createMissingRepositories", true);
+    replicatePermissions = cfg.getBoolean("remote", name, "replicatePermissions", true);
+    replicateProjectDeletions = cfg.getBoolean("remote", name, "replicateProjectDeletions", false);
+    replicateHiddenProjects = cfg.getBoolean("remote", name, "replicateHiddenProjects", false);
+    remoteNameStyle =
+        MoreObjects.firstNonNull(cfg.getString("remote", name, "remoteNameStyle"), "slash");
     maxRetries =
         getInt(
             remoteConfig, cfg, "replicationMaxRetries", cfg.getInt("replication", "maxRetries", 0));
@@ -72,6 +69,10 @@
     return delay;
   }
 
+  public int getRescheduleDelay() {
+    return rescheduleDelay;
+  }
+
   public int getRetryDelay() {
     return retryDelay;
   }
@@ -128,8 +129,7 @@
     return maxRetries;
   }
 
-  private static int getInt(
-      RemoteConfig rc, Config cfg, String name, int defValue) {
+  private static int getInt(RemoteConfig rc, Config cfg, String name, int defValue) {
     return cfg.getInt("remote", rc.getName(), name, defValue);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationFactory.java
new file mode 100644
index 0000000..df886cb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationFactory.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2016 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.common.EventDispatcher;
+import com.google.gerrit.extensions.registration.DynamicItem;
+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.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+
+@Singleton
+public class DestinationFactory {
+  private final Injector injector;
+  private final RemoteSiteUser.Factory replicationUserFactory;
+  private final PluginUser pluginUser;
+  private final GitRepositoryManager gitRepositoryManager;
+  private final GroupBackend groupBackend;
+  private final ReplicationStateListener stateLog;
+  private final GroupIncludeCache groupIncludeCache;
+  private final DynamicItem<EventDispatcher> eventDispatcher;
+
+  @Inject
+  public DestinationFactory(
+      Injector injector,
+      RemoteSiteUser.Factory replicationUserFactory,
+      PluginUser pluginUser,
+      GitRepositoryManager gitRepositoryManager,
+      GroupBackend groupBackend,
+      ReplicationStateListener stateLog,
+      GroupIncludeCache groupIncludeCache,
+      DynamicItem<EventDispatcher> eventDispatcher) {
+    this.injector = injector;
+    this.replicationUserFactory = replicationUserFactory;
+    this.pluginUser = pluginUser;
+    this.gitRepositoryManager = gitRepositoryManager;
+    this.groupBackend = groupBackend;
+    this.stateLog = stateLog;
+    this.groupIncludeCache = groupIncludeCache;
+    this.eventDispatcher = eventDispatcher;
+  }
+
+  Destination create(DestinationConfiguration config) {
+    return new Destination(
+        injector,
+        config,
+        replicationUserFactory,
+        pluginUser,
+        gitRepositoryManager,
+        groupBackend,
+        stateLog,
+        groupIncludeCache,
+        eventDispatcher);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/Init.java b/src/main/java/com/googlesource/gerrit/plugins/replication/Init.java
new file mode 100644
index 0000000..a9fdb4f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Init.java
@@ -0,0 +1,70 @@
+// 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 static com.googlesource.gerrit.plugins.replication.DestinationConfiguration.DEFAULT_REPLICATION_DELAY;
+import static com.googlesource.gerrit.plugins.replication.DestinationConfiguration.DEFAULT_RESCHEDULE_DELAY;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import java.io.File;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+public class Init implements InitStep {
+  private final String pluginName;
+  private final SitePaths site;
+  private final ConsoleUI ui;
+
+  @Inject
+  Init(@PluginName String pluginName, SitePaths site, ConsoleUI ui) {
+    this.pluginName = pluginName;
+    this.site = site;
+    this.ui = ui;
+  }
+
+  @Override
+  public void run() throws Exception {
+    File configFile = site.etc_dir.resolve(pluginName + ".config").toFile();
+    if (!configFile.exists()) {
+      return;
+    }
+
+    FileBasedConfig config = new FileBasedConfig(configFile, FS.DETECTED);
+    config.load();
+    for (String name : config.getSubsections("remote")) {
+      if (!Strings.isNullOrEmpty(config.getString("remote", name, "rescheduleDelay"))) {
+        continue;
+      }
+
+      int replicationDelay =
+          config.getInt("remote", name, "replicationDelay", DEFAULT_REPLICATION_DELAY);
+      if (replicationDelay > 0) {
+        int delay = Math.max(replicationDelay, DEFAULT_RESCHEDULE_DELAY);
+        ui.message("Setting remote.%s.rescheduleDelay = %d\n", name, delay);
+        config.setInt("remote", name, "rescheduleDelay", delay);
+      } else {
+        ui.message(
+            "INFO: Assuming default (%d s) for remote.%s.rescheduleDelay\n",
+            DEFAULT_RESCHEDULE_DELAY, name);
+      }
+    }
+    config.save();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java
index 247ebf7..fa17dce 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java
@@ -23,13 +23,10 @@
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 import com.google.inject.Inject;
-
 import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType;
-
-import org.kohsuke.args4j.Option;
-
 import java.util.Collection;
 import java.util.List;
+import org.kohsuke.args4j.Option;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "list", description = "List remote destination information")
@@ -43,8 +40,7 @@
   @Option(name = "--json", usage = "output in json format")
   private boolean json;
 
-  @Inject
-  private ReplicationConfig config;
+  @Inject private ReplicationConfig config;
 
   @Override
   protected void run() {
@@ -56,9 +52,7 @@
   }
 
   private boolean matches(String name) {
-    return (Strings.isNullOrEmpty(remote)
-        || name.contains(remote)
-        || name.matches(remote));
+    return (Strings.isNullOrEmpty(remote) || name.contains(remote) || name.matches(remote));
   }
 
   private void addProperty(JsonObject obj, String key, List<String> values) {
@@ -73,14 +67,11 @@
 
   private void addQueueDetails(StringBuilder out, Collection<PushOne> values) {
     for (PushOne p : values) {
-      out.append("  ")
-        .append(p.toString())
-        .append("\n");
+      out.append("  ").append(p.toString()).append("\n");
     }
   }
 
-  private void addQueueDetails(JsonObject obj, String key,
-      Collection<PushOne> values) {
+  private void addQueueDetails(JsonObject obj, String key, Collection<PushOne> values) {
     if (values.size() > 0) {
       JsonArray list = new JsonArray();
       for (PushOne p : values) {
@@ -106,42 +97,28 @@
       stdout.print(obj.toString() + "\n");
     } else {
       StringBuilder out = new StringBuilder();
-      out.append("Remote: ")
-        .append(d.getRemoteConfigName())
-        .append("\n");
+      out.append("Remote: ").append(d.getRemoteConfigName()).append("\n");
       for (String url : d.getUrls()) {
-        out.append("Url: ")
-          .append(url)
-          .append("\n");
+        out.append("Url: ").append(url).append("\n");
       }
 
       if (detail) {
         for (String adminUrl : d.getAdminUrls()) {
-          out.append("AdminUrl: ")
-            .append(adminUrl)
-            .append("\n");
+          out.append("AdminUrl: ").append(adminUrl).append("\n");
         }
 
         for (String authGroup : d.getAuthGroupNames()) {
-          out.append("AuthGroup: ")
-            .append(authGroup)
-            .append("\n");
+          out.append("AuthGroup: ").append(authGroup).append("\n");
         }
 
         for (String project : d.getProjects()) {
-          out.append("Project: ")
-            .append(project)
-            .append("\n");
+          out.append("Project: ").append(project).append("\n");
         }
 
         Destination.QueueInfo q = d.getQueueInfo();
-        out.append("In Flight: ")
-          .append(q.inFlight.size())
-          .append("\n");
+        out.append("In Flight: ").append(q.inFlight.size()).append("\n");
         addQueueDetails(out, q.inFlight.values());
-        out.append("Pending: ")
-          .append(q.pending.size())
-          .append("\n");
+        out.append("Pending: ").append(q.pending.size()).append("\n");
         addQueueDetails(out, q.pending.values());
       }
       stdout.print(out.toString() + "\n");
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 cad7bdd..a6b38c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
@@ -20,9 +20,7 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.inject.Inject;
-
 import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
-
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -56,10 +54,9 @@
 
     if (srvInfo.getState() == ServerInformation.State.STARTUP
         && config.isReplicateAllOnPluginStart()) {
-      ReplicationState state = new ReplicationState(
-          new GitUpdateProcessing(eventDispatcher.get()));
-      pushAllFuture.set(pushAll.create(
-          null, ReplicationFilter.all(), state).schedule(30, TimeUnit.SECONDS));
+      ReplicationState state = new ReplicationState(new GitUpdateProcessing(eventDispatcher.get()));
+      pushAllFuture.set(
+          pushAll.create(null, ReplicationFilter.all(), state).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 16e1678..da32ecd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
@@ -28,9 +27,7 @@
   private final ReplicationStateListener stateLog;
 
   public interface Factory {
-    PushAll create(String urlMatch,
-        ReplicationFilter filter,
-        ReplicationState state);
+    PushAll create(String urlMatch, ReplicationFilter filter, ReplicationState state);
   }
 
   private final WorkQueue workQueue;
@@ -41,7 +38,8 @@
   private final ReplicationState state;
 
   @Inject
-  protected PushAll(WorkQueue wq,
+  protected PushAll(
+      WorkQueue wq,
       ProjectCache projectCache,
       ReplicationQueue rq,
       ReplicationStateListener stateLog,
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 46a87a4..525c990 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -19,7 +19,7 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.Multimap;
+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;
@@ -42,10 +42,19 @@
 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;
 import com.jcraft.jsch.JSchException;
-
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.RemoteRepositoryException;
@@ -65,23 +74,11 @@
 import org.eclipse.jgit.transport.URIish;
 import org.slf4j.MDC;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 /**
  * A push to remote operation started by {@link GitReferenceUpdatedListener}.
- * <p>
- * Instance members are protected by the lock within PushQueue. Callers must
- * take that lock to ensure they are working with a current view of the object.
+ *
+ * <p>Instance members are protected by the lock within PushQueue. Callers must take that lock to
+ * ensure they are working with a current view of the object.
  */
 class PushOne implements ProjectRunnable, CanceledWhileRunning {
   private final ReplicationStateListener stateLog;
@@ -112,8 +109,7 @@
   private int retryCount;
   private final int maxRetries;
   private boolean canceled;
-  private final Multimap<String,ReplicationState> stateMap =
-      LinkedListMultimap.create();
+  private final ListMultimap<String, ReplicationState> stateMap = LinkedListMultimap.create();
   private final int maxLockRetries;
   private int lockRetryCount;
   private final int id;
@@ -122,7 +118,8 @@
   private final AtomicBoolean canceledWhileRunning;
 
   @Inject
-  PushOne(GitRepositoryManager grm,
+  PushOne(
+      GitRepositoryManager grm,
       SchemaFactory<ReviewDb> s,
       Destination p,
       RemoteConfig c,
@@ -246,7 +243,7 @@
     stateMap.put(ref, state);
   }
 
-  Multimap<String,ReplicationState> getStates() {
+  ListMultimap<String, ReplicationState> getStates() {
     return stateMap;
   }
 
@@ -261,7 +258,7 @@
     return states.toArray(new ReplicationState[states.size()]);
   }
 
-  void addStates(Multimap<String,ReplicationState> states) {
+  void addStates(ListMultimap<String, ReplicationState> states) {
     stateMap.putAll(states);
   }
 
@@ -271,9 +268,11 @@
 
   private void statesCleanUp() {
     if (!stateMap.isEmpty() && !isRetrying()) {
-      for (Map.Entry<String,ReplicationState> entry : stateMap.entries()) {
-        entry.getValue().notifyRefReplicated(projectName.get(), entry.getKey(), uri,
-            RefPushResult.FAILED, null);
+      for (Map.Entry<String, ReplicationState> entry : stateMap.entries()) {
+        entry
+            .getValue()
+            .notifyRefReplicated(
+                projectName.get(), entry.getKey(), uri, RefPushResult.FAILED, null);
       }
     }
   }
@@ -281,15 +280,18 @@
   @Override
   public void run() {
     try {
-      threadScoper.scope(new Callable<Void>() {
-        @Override
-        public Void call() {
-          runPushOperation();
-          return null;
-        }
-      }).call();
+      threadScoper
+          .scope(
+              new Callable<Void>() {
+                @Override
+                public Void call() {
+                  runPushOperation();
+                  return null;
+                }
+              })
+          .call();
     } catch (Exception e) {
-      Throwables.propagateIfPossible(e);
+      Throwables.throwIfUnchecked(e);
       throw new RuntimeException(e);
     } finally {
       statesCleanUp();
@@ -304,8 +306,8 @@
     MDC.put(ID_MDC_KEY, IdGenerator.format(id));
     if (!pool.requestRunway(this)) {
       if (!canceled) {
-        repLog.info("Rescheduling replication to " + uri
-            + " to avoid collision with an in-flight push.");
+        repLog.info(
+            "Rescheduling replication to " + uri + " to avoid collision with an in-flight push.");
         pool.reschedule(this, Destination.RetryReason.COLLISION);
       }
       return;
@@ -320,13 +322,20 @@
       git = gitManager.openRepository(projectName);
       runImpl();
       long elapsed = NANOSECONDS.toMillis(context.stop());
-      repLog.info("Replication to " + uri + " completed in "
-          + (elapsed) + "ms, "
-          + (delay) + "ms delay, " + retryCount + " retries");
+      repLog.info(
+          "Replication to "
+              + uri
+              + " completed in "
+              + (elapsed)
+              + "ms, "
+              + (delay)
+              + "ms delay, "
+              + retryCount
+              + " retries");
     } catch (RepositoryNotFoundException e) {
-      stateLog.error("Cannot replicate " + projectName
-          + "; Local repository error: "
-          + e.getMessage(), getStatesAsArray());
+      stateLog.error(
+          "Cannot replicate " + projectName + "; Local repository error: " + e.getMessage(),
+          getStatesAsArray());
 
     } catch (RemoteRepositoryException e) {
       // Tried to replicate to a remote via anonymous git:// but the repository
@@ -336,8 +345,7 @@
       if (msg.contains("access denied") || msg.contains("no such repository")) {
         createRepository();
       } else {
-        repLog.error("Cannot replicate " + projectName
-            + "; Remote repository error: " + msg);
+        repLog.error("Cannot replicate " + projectName + "; Remote repository error: " + msg);
       }
 
     } catch (NoRemoteRepositoryException e) {
@@ -346,8 +354,7 @@
       stateLog.error("Cannot replicate to " + uri, e, getStatesAsArray());
     } catch (TransportException e) {
       Throwable cause = e.getCause();
-      if (cause instanceof JSchException
-          && cause.getMessage().startsWith("UnknownHostKey:")) {
+      if (cause instanceof JSchException && cause.getMessage().startsWith("UnknownHostKey:")) {
         repLog.error("Cannot replicate to " + uri + ": " + cause.getMessage());
       } else if (e instanceof LockFailureException) {
         lockRetryCount++;
@@ -363,8 +370,11 @@
             pool.reschedule(this, Destination.RetryReason.TRANSPORT_ERROR);
           }
         } else {
-          repLog.error("Giving up after " + lockRetryCount
-              + " of this error during replication to " + e.getMessage());
+          repLog.error(
+              "Giving up after "
+                  + lockRetryCount
+                  + " of this error during replication to "
+                  + e.getMessage());
         }
       } else {
         if (canceledWhileRunning.get()) {
@@ -388,8 +398,7 @@
   }
 
   private void logCanceledWhileRunningException(TransportException e) {
-    repLog.info("Cannot replicate to " + uri + "."
-        + " It was canceled while running", e);
+    repLog.info("Cannot replicate to " + uri + "." + " It was canceled while running", e);
   }
 
   private void createRepository() {
@@ -400,13 +409,17 @@
           repLog.warn("Missing repository created; retry replication to " + uri);
           pool.reschedule(this, Destination.RetryReason.REPOSITORY_MISSING);
         } else {
-          repLog.warn("Missing repository could not be created when replicating " + uri +
-              ". You can only create missing repositories locally, over SSH or when " +
-              "using adminUrl in replication.config. See documentation for more information.");
+          repLog.warn(
+              "Missing repository could not be created when replicating "
+                  + uri
+                  + ". You can only create missing repositories locally, over SSH or when "
+                  + "using adminUrl in replication.config. See documentation for more information.");
         }
       } catch (IOException ioe) {
-        stateLog.error("Cannot replicate to " + uri + "; failed to create missing repository",
-            ioe, getStatesAsArray());
+        stateLog.error(
+            "Cannot replicate to " + uri + "; failed to create missing repository",
+            ioe,
+            getStatesAsArray());
       }
     } else {
       stateLog.error("Cannot replicate to " + uri + "; repository not found", getStatesAsArray());
@@ -440,8 +453,7 @@
     return tn.push(NullProgressMonitor.INSTANCE, todo);
   }
 
-  private List<RemoteRefUpdate> generateUpdates(Transport tn)
-      throws IOException {
+  private List<RemoteRefUpdate> generateUpdates(Transport tn) throws IOException {
     ProjectControl pc;
     try {
       pc = pool.controlFor(projectName);
@@ -466,11 +478,12 @@
       }
 
       try (ReviewDb db = schema.open()) {
-        local = new VisibleRefFilter(
-                  tagCache, changeNotesFactory, changeCache, git, pc, db, true)
-            .filter(local, true);
+        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());
+        stateLog.error(
+            "Cannot read database to replicate to " + projectName, e, getStatesAsArray());
         return Collections.emptyList();
       }
     }
@@ -512,8 +525,7 @@
     return cmds;
   }
 
-  private List<RemoteRefUpdate> doPushDelta(Map<String, Ref> local)
-      throws IOException {
+  private List<RemoteRefUpdate> doPushDelta(Map<String, Ref> local) throws IOException {
     List<RemoteRefUpdate> cmds = new ArrayList<>();
     boolean noPerms = !pool.isReplicatePermissions();
     for (String src : delta) {
@@ -538,8 +550,8 @@
   }
 
   private boolean canPushRef(String ref, boolean noPerms) {
-    return !(noPerms && RefNames.REFS_CONFIG.equals(ref)) &&
-        !ref.startsWith(RefNames.REFS_CACHE_AUTOMERGE);
+    return !(noPerms && RefNames.REFS_CONFIG.equals(ref))
+        && !ref.startsWith(RefNames.REFS_CACHE_AUTOMERGE);
   }
 
   private Map<String, Ref> listRemote(Transport tn)
@@ -567,22 +579,19 @@
     return null;
   }
 
-  private void push(List<RemoteRefUpdate> cmds, RefSpec spec, Ref src)
-      throws IOException {
+  private void push(List<RemoteRefUpdate> cmds, RefSpec spec, Ref src) throws IOException {
     String dst = spec.getDestination();
     boolean force = spec.isForceUpdate();
     cmds.add(new RemoteRefUpdate(git, src, dst, force, null, null));
   }
 
-  private void delete(List<RemoteRefUpdate> cmds, RefSpec spec)
-      throws IOException {
+  private void delete(List<RemoteRefUpdate> cmds, RefSpec spec) throws IOException {
     String dst = spec.getDestination();
     boolean force = spec.isForceUpdate();
     cmds.add(new RemoteRefUpdate(git, (Ref) null, dst, force, null, null));
   }
 
-  private void updateStates(Collection<RemoteRefUpdate> refUpdates)
-      throws LockFailureException {
+  private void updateStates(Collection<RemoteRefUpdate> refUpdates) throws LockFailureException {
     Set<String> doneRefs = new HashSet<>();
     boolean anyRefFailed = false;
     RemoteRefUpdate.Status lastRefStatusError = RemoteRefUpdate.Status.OK;
@@ -607,8 +616,10 @@
         case REJECTED_NODELETE:
         case REJECTED_NONFASTFORWARD:
         case REJECTED_REMOTE_CHANGED:
-          stateLog.error(String.format("Failed replicate of %s to %s: status %s",
-              u.getRemoteName(), uri, u.getStatus()), logStatesArray);
+          stateLog.error(
+              String.format(
+                  "Failed replicate of %s to %s: status %s", u.getRemoteName(), uri, u.getStatus()),
+              logStatesArray);
           pushStatus = RefPushResult.FAILED;
           anyRefFailed = true;
           lastRefStatusError = u.getStatus();
@@ -616,16 +627,22 @@
 
         case REJECTED_OTHER_REASON:
           if ("non-fast-forward".equals(u.getMessage())) {
-            stateLog.error(String.format("Failed replicate of %s to %s"
-                + ", remote rejected non-fast-forward push."
-                + "  Check receive.denyNonFastForwards variable in config file"
-                + " of destination repository.", u.getRemoteName(), uri), logStatesArray);
+            stateLog.error(
+                String.format(
+                    "Failed replicate of %s to %s"
+                        + ", remote rejected non-fast-forward push."
+                        + "  Check receive.denyNonFastForwards variable in config file"
+                        + " of destination repository.",
+                    u.getRemoteName(), uri),
+                logStatesArray);
           } else if ("failed to lock".equals(u.getMessage())) {
             throw new LockFailureException(uri, u.getMessage());
           } else {
-            stateLog.error(String.format(
-                "Failed replicate of %s to %s, reason: %s",
-                u.getRemoteName(), uri, u.getMessage()), logStatesArray);
+            stateLog.error(
+                String.format(
+                    "Failed replicate of %s to %s, reason: %s",
+                    u.getRemoteName(), uri, u.getMessage()),
+                logStatesArray);
           }
           pushStatus = RefPushResult.FAILED;
           anyRefFailed = true;
@@ -634,20 +651,25 @@
       }
 
       for (ReplicationState rs : getStatesByRef(u.getSrcRef())) {
-        rs.notifyRefReplicated(projectName.get(), u.getSrcRef(),
-              uri, pushStatus, u.getStatus());
+        rs.notifyRefReplicated(projectName.get(), u.getSrcRef(), uri, pushStatus, u.getStatus());
       }
     }
 
     doneRefs.add(ALL_REFS);
     for (ReplicationState rs : getStatesByRef(ALL_REFS)) {
-      rs.notifyRefReplicated(projectName.get(), ALL_REFS, uri, anyRefFailed
-          ? RefPushResult.FAILED : RefPushResult.SUCCEEDED, lastRefStatusError);
+      rs.notifyRefReplicated(
+          projectName.get(),
+          ALL_REFS,
+          uri,
+          anyRefFailed ? RefPushResult.FAILED : RefPushResult.SUCCEEDED,
+          lastRefStatusError);
     }
     for (Map.Entry<String, ReplicationState> entry : stateMap.entries()) {
       if (!doneRefs.contains(entry.getKey())) {
-        entry.getValue().notifyRefReplicated(projectName.get(), entry.getKey(),
-            uri, RefPushResult.NOT_ATTEMPTED, null);
+        entry
+            .getValue()
+            .notifyRefReplicated(
+                projectName.get(), entry.getKey(), uri, RefPushResult.NOT_ATTEMPTED, null);
       }
     }
     stateMap.clear();
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 6717660..0c3e158 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
@@ -17,21 +17,22 @@
 import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.server.events.RefEvent;
 import com.google.gwtorm.server.OrmException;
-
 import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
-
+import java.lang.ref.WeakReference;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.URIish;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.lang.ref.WeakReference;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 public abstract class PushResultProcessing {
 
-  abstract void onRefReplicatedToOneNode(String project, String ref,
-      URIish uri, RefPushResult status, RemoteRefUpdate.Status refStatus);
+  abstract void onRefReplicatedToOneNode(
+      String project,
+      String ref,
+      URIish uri,
+      RefPushResult status,
+      RemoteRefUpdate.Status refStatus);
 
   abstract void onRefReplicatedToAllNodes(String project, String ref, int nodesCount);
 
@@ -55,7 +56,7 @@
     // Default doing nothing
   }
 
-  private static String resolveNodeName(URIish uri) {
+  static String resolveNodeName(URIish uri) {
     StringBuilder sb = new StringBuilder();
     if (uri.isRemote()) {
       sb.append(uri.getHost());
@@ -78,8 +79,12 @@
     }
 
     @Override
-    void onRefReplicatedToOneNode(String project, String ref, URIish uri,
-        RefPushResult status, RemoteRefUpdate.Status refStatus) {
+    void onRefReplicatedToOneNode(
+        String project,
+        String ref,
+        URIish uri,
+        RefPushResult status,
+        RemoteRefUpdate.Status refStatus) {
       StringBuilder sb = new StringBuilder();
       sb.append("Replicate ");
       sb.append(project);
@@ -162,10 +167,13 @@
     }
 
     @Override
-    void onRefReplicatedToOneNode(String project, String ref, URIish uri,
-        RefPushResult status, RemoteRefUpdate.Status refStatus) {
-      postEvent(new RefReplicatedEvent(project, ref, resolveNodeName(uri),
-          status, refStatus));
+    void onRefReplicatedToOneNode(
+        String project,
+        String ref,
+        URIish uri,
+        RefPushResult status,
+        RemoteRefUpdate.Status refStatus) {
+      postEvent(new RefReplicatedEvent(project, ref, resolveNodeName(uri), status, refStatus));
     }
 
     @Override
@@ -174,8 +182,7 @@
     }
 
     @Override
-    void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
-    }
+    void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {}
 
     private void postEvent(RefEvent event) {
       try {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
index a1c5596..364f1b4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
@@ -16,9 +16,7 @@
 
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.events.RefEvent;
-
 import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
-
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 
@@ -31,8 +29,12 @@
   final String status;
   final Status refStatus;
 
-  public RefReplicatedEvent(String project, String ref, String targetNode,
-      RefPushResult status, RemoteRefUpdate.Status refStatus) {
+  public RefReplicatedEvent(
+      String project,
+      String ref,
+      String targetNode,
+      RefPushResult status,
+      RemoteRefUpdate.Status refStatus) {
     super(TYPE);
     this.project = project;
     this.ref = ref;
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 11253c6..f3dc04d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSiteUser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSiteUser.java
@@ -28,8 +28,8 @@
   private final GroupMembership effectiveGroups;
 
   @Inject
-  RemoteSiteUser(CapabilityControl.Factory capabilityControlFactory,
-      @Assisted GroupMembership authGroups) {
+  RemoteSiteUser(
+      CapabilityControl.Factory capabilityControlFactory, @Assisted GroupMembership authGroups) {
     super(capabilityControlFactory);
     effectiveGroups = authGroups;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
index 241c881..e94abbd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
@@ -14,7 +14,6 @@
 package com.googlesource.gerrit.plugins.replication;
 
 import com.google.gerrit.server.git.WorkQueue;
-
 import java.util.List;
 
 public interface ReplicationConfig {
@@ -36,5 +35,4 @@
   int shutdown();
 
   void startup(WorkQueue workQueue);
-
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
index 4b976bc..82e68ed 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
@@ -13,20 +13,22 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.replication;
 
-import com.google.common.base.Predicate;
-import com.google.common.collect.FluentIterable;
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-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.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
 import com.google.inject.Singleton;
-
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.RefSpec;
@@ -36,13 +38,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.nio.file.Path;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
 @Singleton
 public class ReplicationFileBasedConfig implements ReplicationConfig {
   static final Logger log = LoggerFactory.getLogger(ReplicationFileBasedConfig.class);
@@ -50,35 +45,14 @@
   private Path cfgPath;
   private boolean replicateAllOnPluginStart;
   private boolean defaultForceUpdate;
-  private Injector injector;
-  private final RemoteSiteUser.Factory replicationUserFactory;
-  private final PluginUser pluginUser;
-  private final GitRepositoryManager gitRepositoryManager;
-  private final GroupBackend groupBackend;
   private final FileBasedConfig config;
-  private final ReplicationStateListener stateLog;
-  private final GroupIncludeCache groupIncludeCache;
 
   @Inject
-  public ReplicationFileBasedConfig(Injector injector,
-      SitePaths site,
-      RemoteSiteUser.Factory ruf,
-      PluginUser pu,
-      GitRepositoryManager grm,
-      GroupBackend gb,
-      ReplicationStateListener stateLog,
-      GroupIncludeCache groupIncludeCache)
+  public ReplicationFileBasedConfig(SitePaths site, DestinationFactory destinationFactory)
       throws ConfigInvalidException, IOException {
     this.cfgPath = site.etc_dir.resolve("replication.config");
-    this.groupIncludeCache = groupIncludeCache;
-    this.injector = injector;
-    this.replicationUserFactory = ruf;
-    this.pluginUser = pu;
-    this.gitRepositoryManager = grm;
-    this.groupBackend = gb;
     this.config = new FileBasedConfig(cfgPath.toFile(), FS.DETECTED);
-    this.destinations = allDestinations();
-    this.stateLog = stateLog;
+    this.destinations = allDestinations(destinationFactory);
   }
 
   /*
@@ -89,40 +63,23 @@
    */
   @Override
   public List<Destination> getDestinations(FilterType filterType) {
-    Predicate<Destination> filter;
+    Predicate<? super Destination> filter;
     switch (filterType) {
-      case PROJECT_CREATION :
-        filter = new Predicate<Destination>() {
-
-          @Override
-          public boolean apply(Destination dest) {
-            if (dest == null || !dest.isCreateMissingRepos()) {
-              return false;
-            }
-            return true;
-          }
-        };
+      case PROJECT_CREATION:
+        filter = dest -> dest.isCreateMissingRepos();
         break;
-      case PROJECT_DELETION :
-        filter = new Predicate<Destination>() {
-
-          @Override
-          public boolean apply(Destination dest) {
-            if (dest == null || !dest.isReplicateProjectDeletions()) {
-              return false;
-            }
-            return true;
-          }
-        };
+      case PROJECT_DELETION:
+        filter = dest -> dest.isReplicateProjectDeletions();
         break;
-      case ALL :
-        return destinations;
-      default :
-        return destinations;
+      case ALL:
+      default:
+        filter = dest -> true;
+        break;
     }
-    return FluentIterable.from(destinations).filter(filter).toList();
+    return destinations.stream().filter(Objects::nonNull).filter(filter).collect(toList());
   }
-  private List<Destination> allDestinations()
+
+  private List<Destination> allDestinations(DestinationFactory destinationFactory)
       throws ConfigInvalidException, IOException {
     if (!config.getFile().exists()) {
       log.warn("Config file " + config.getFile() + " does not exist; not replicating");
@@ -136,18 +93,16 @@
     try {
       config.load();
     } catch (ConfigInvalidException e) {
-      throw new ConfigInvalidException(String.format(
-          "Config file %s is invalid: %s", config.getFile(), e.getMessage()), e);
+      throw new ConfigInvalidException(
+          String.format("Config file %s is invalid: %s", config.getFile(), e.getMessage()), e);
     } catch (IOException e) {
-      throw new IOException(String.format("Cannot read %s: %s", config.getFile(),
-          e.getMessage()), e);
+      throw new IOException(
+          String.format("Cannot read %s: %s", config.getFile(), e.getMessage()), e);
     }
 
-    replicateAllOnPluginStart =
-        config.getBoolean("gerrit", "replicateOnStartup", true);
+    replicateAllOnPluginStart = config.getBoolean("gerrit", "replicateOnStartup", true);
 
-    defaultForceUpdate =
-        config.getBoolean("gerrit", "defaultForceUpdate", false);
+    defaultForceUpdate = config.getBoolean("gerrit", "defaultForceUpdate", false);
 
     ImmutableList.Builder<Destination> dest = ImmutableList.builder();
     for (RemoteConfig c : allRemotes(config)) {
@@ -163,21 +118,21 @@
       }
 
       if (c.getPushRefSpecs().isEmpty()) {
-        c.addPushRefSpec(new RefSpec().setSourceDestination("refs/*", "refs/*")
-            .setForceUpdate(defaultForceUpdate));
+        c.addPushRefSpec(
+            new RefSpec()
+                .setSourceDestination("refs/*", "refs/*")
+                .setForceUpdate(defaultForceUpdate));
       }
 
-      Destination destination =
-          new Destination(injector, new DestinationConfiguration(c,
-              config), replicationUserFactory, pluginUser,
-              gitRepositoryManager, groupBackend, stateLog, groupIncludeCache);
+      Destination destination = destinationFactory.create(new DestinationConfiguration(c, config));
 
       if (!destination.isSingleProjectMatch()) {
         for (URIish u : c.getURIs()) {
           if (u.getPath() == null || !u.getPath().contains("${name}")) {
-            throw new ConfigInvalidException(String.format(
-                "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
-                c.getName(), u, config.getFile()));
+            throw new ConfigInvalidException(
+                String.format(
+                    "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
+                    c.getName(), u, config.getFile()));
           }
         }
       }
@@ -203,16 +158,15 @@
     return defaultForceUpdate;
   }
 
-  private static List<RemoteConfig> allRemotes(FileBasedConfig cfg)
-      throws ConfigInvalidException {
+  private static List<RemoteConfig> allRemotes(FileBasedConfig cfg) throws ConfigInvalidException {
     Set<String> names = cfg.getSubsections("remote");
     List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
     for (String name : names) {
       try {
         result.add(new RemoteConfig(cfg, name));
       } catch (URISyntaxException e) {
-        throw new ConfigInvalidException(String.format(
-            "remote %s has invalid URL in %s", name, cfg.getFile()));
+        throw new ConfigInvalidException(
+            String.format("remote %s has invalid URL in %s", name, cfg.getFile()));
       }
     }
     return result;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFilter.java
index 6b75d3e..7b3486b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFilter.java
@@ -16,17 +16,18 @@
 
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
-
 import java.util.Collections;
 import java.util.List;
 
 public class ReplicationFilter {
   public enum PatternType {
-    REGEX, WILDCARD, EXACT_MATCH;
+    REGEX,
+    WILDCARD,
+    EXACT_MATCH;
   }
 
   public static ReplicationFilter all() {
-    return new ReplicationFilter(Collections.<String> emptyList());
+    return new ReplicationFilter(Collections.<String>emptyList());
   }
 
   public static PatternType getPatternType(String pattern) {
@@ -66,8 +67,7 @@
         match = projectName.matches(pattern);
         break;
       case WILDCARD:
-        match =
-            projectName.startsWith(pattern.substring(0, pattern.length() - 1));
+        match = projectName.startsWith(pattern.substring(0, pattern.length() - 1));
         break;
       case EXACT_MATCH:
         match = projectName.equals(pattern);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java
index fcc3437..fed09f9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java
@@ -18,15 +18,16 @@
 import com.google.gerrit.server.util.PluginLogFile;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.inject.Inject;
-
 import org.apache.log4j.PatternLayout;
 
 public class ReplicationLogFile extends PluginLogFile {
 
   @Inject
-  public ReplicationLogFile(SystemLog systemLog,
-      ServerInformation serverInfo) {
-    super(systemLog, serverInfo, ReplicationQueue.REPLICATION_LOG_NAME,
+  public ReplicationLogFile(SystemLog systemLog, ServerInformation serverInfo) {
+    super(
+        systemLog,
+        serverInfo,
+        ReplicationQueue.REPLICATION_LOG_NAME,
         new PatternLayout("[%d] [%X{" + PushOne.ID_MDC_KEY + "}] %m%n"));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java
index a7b27a0..afc7926 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java
@@ -32,30 +32,34 @@
   ReplicationMetrics(MetricMaker metricMaker) {
     Field<String> DEST_FIELD = Field.ofString("destination");
 
-    executionTime = metricMaker.newTimer(
-        "replication_latency",
-        new Description("Time spent pushing to remote destination.")
-          .setCumulative()
-          .setUnit(Description.Units.MILLISECONDS),
-        DEST_FIELD);
+    executionTime =
+        metricMaker.newTimer(
+            "replication_latency",
+            new Description("Time spent pushing to remote destination.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS),
+            DEST_FIELD);
 
-    executionDelay = metricMaker.newHistogram(
-        "replication_delay",
-        new Description("Time spent waiting before pushing to remote destination")
-          .setCumulative()
-          .setUnit(Description.Units.MILLISECONDS),
-        DEST_FIELD);
+    executionDelay =
+        metricMaker.newHistogram(
+            "replication_delay",
+            new Description("Time spent waiting before pushing to remote destination")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS),
+            DEST_FIELD);
 
-    executionRetries = metricMaker.newHistogram(
-        "replication_retries",
-        new Description("Number of retries when pushing to remote destination")
-          .setCumulative()
-          .setUnit("retries"),
-        DEST_FIELD);
+    executionRetries =
+        metricMaker.newHistogram(
+            "replication_retries",
+            new Description("Number of retries when pushing to remote destination")
+                .setCumulative()
+                .setUnit("retries"),
+            DEST_FIELD);
   }
 
   /**
    * Start the replication latency timer for a destination.
+   *
    * @param name the destination name.
    * @return the timer context.
    */
@@ -65,6 +69,7 @@
 
   /**
    * Record the replication delay and retry metrics for a destination.
+   *
    * @param name the destination name.
    * @param delay replication delay in milliseconds.
    * @param retries number of retries.
@@ -73,5 +78,4 @@
     executionDelay.record(name, delay);
     executionRetries.record(name, retries);
   }
-
 }
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 5a5f3b4..f30e13d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -29,34 +29,30 @@
 import com.google.inject.Scopes;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.internal.UniqueAnnotations;
-
 import org.eclipse.jgit.transport.SshSessionFactory;
 
 class ReplicationModule extends AbstractModule {
   @Override
   protected void configure() {
+    bind(DestinationFactory.class).in(Scopes.SINGLETON);
     bind(ReplicationQueue.class).in(Scopes.SINGLETON);
 
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
-        .to(ReplicationQueue.class);
-    DynamicSet.bind(binder(), NewProjectCreatedListener.class)
-        .to(ReplicationQueue.class);
-    DynamicSet.bind(binder(), ProjectDeletedListener.class)
-        .to(ReplicationQueue.class);
-    DynamicSet.bind(binder(), HeadUpdatedListener.class)
-        .to(ReplicationQueue.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReplicationQueue.class);
+    DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(ReplicationQueue.class);
+    DynamicSet.bind(binder(), ProjectDeletedListener.class).to(ReplicationQueue.class);
+    DynamicSet.bind(binder(), HeadUpdatedListener.class).to(ReplicationQueue.class);
 
     bind(OnStartStop.class).in(Scopes.SINGLETON);
+    bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create()).to(OnStartStop.class);
     bind(LifecycleListener.class)
-      .annotatedWith(UniqueAnnotations.create())
-      .to(OnStartStop.class);
-    bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create()).to(
-        ReplicationLogFile.class);
-    bind(CredentialsFactory.class).to(
-        AutoReloadSecureCredentialsFactoryDecorator.class).in(Scopes.SINGLETON);
+        .annotatedWith(UniqueAnnotations.create())
+        .to(ReplicationLogFile.class);
+    bind(CredentialsFactory.class)
+        .to(AutoReloadSecureCredentialsFactoryDecorator.class)
+        .in(Scopes.SINGLETON);
     bind(CapabilityDefinition.class)
-      .annotatedWith(Exports.named(START_REPLICATION))
-      .to(StartReplicationCapability.class);
+        .annotatedWith(Exports.named(START_REPLICATION))
+        .to(StartReplicationCapability.class);
 
     install(new FactoryModuleBuilder().build(PushAll.Factory.class));
     install(new FactoryModuleBuilder().build(RemoteSiteUser.Factory.class));
@@ -66,7 +62,7 @@
 
     EventTypes.register(RefReplicatedEvent.TYPE, RefReplicatedEvent.class);
     EventTypes.register(RefReplicationDoneEvent.TYPE, RefReplicationDoneEvent.class);
-    bind(SshSessionFactory.class).toProvider(
-        ReplicationSshSessionFactoryProvider.class);
+    EventTypes.register(ReplicationScheduledEvent.TYPE, ReplicationScheduledEvent.class);
+    bind(SshSessionFactory.class).toProvider(ReplicationSshSessionFactoryProvider.class);
   }
 }
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 ba8e9cc..9a68d32 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -26,10 +26,15 @@
 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;
+import java.io.IOException;
+import java.io.OutputStream;
+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;
@@ -44,21 +49,13 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.URISyntaxException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
 /** Manages automatic replication to remote repositories. */
-public class ReplicationQueue implements
-    LifecycleListener,
-    GitReferenceUpdatedListener,
-    NewProjectCreatedListener,
-    ProjectDeletedListener,
-    HeadUpdatedListener {
+public class ReplicationQueue
+    implements LifecycleListener,
+        GitReferenceUpdatedListener,
+        NewProjectCreatedListener,
+        ProjectDeletedListener,
+        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;
@@ -84,7 +81,8 @@
   private volatile boolean running;
 
   @Inject
-  ReplicationQueue(WorkQueue wq,
+  ReplicationQueue(
+      WorkQueue wq,
       ReplicationConfig rc,
       DynamicItem<EventDispatcher> dis,
       ReplicationStateListener sl,
@@ -107,16 +105,14 @@
     running = false;
     int discarded = config.shutdown();
     if (discarded > 0) {
-      repLog.warn(String.format(
-          "Canceled %d replication events during shutdown", discarded));
+      repLog.warn(String.format("Canceled %d replication events during shutdown", discarded));
     }
   }
 
-  void scheduleFullSync(final Project.NameKey project, final String urlMatch,
-      ReplicationState state) {
+  void scheduleFullSync(
+      final Project.NameKey project, final String urlMatch, ReplicationState state) {
     if (!running) {
-      stateLog.warn("Replication plugin did not finish startup before event",
-          state);
+      stateLog.warn("Replication plugin did not finish startup before event", state);
       return;
     }
 
@@ -131,8 +127,7 @@
 
   @Override
   public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    ReplicationState state =
-        new ReplicationState(new GitUpdateProcessing(dispatcher.get()));
+    ReplicationState state = new ReplicationState(new GitUpdateProcessing(dispatcher.get()));
     if (!running) {
       stateLog.warn("Replication plugin did not finish startup before event", state);
       return;
@@ -151,30 +146,28 @@
 
   @Override
   public void onNewProjectCreated(NewProjectCreatedListener.Event event) {
-    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()),
-        FilterType.PROJECT_CREATION)) {
+    for (URIish uri :
+        getURIs(new Project.NameKey(event.getProjectName()), FilterType.PROJECT_CREATION)) {
       createProject(uri, event.getHeadName());
     }
   }
 
   @Override
   public void onProjectDeleted(ProjectDeletedListener.Event event) {
-    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()),
-        FilterType.PROJECT_DELETION)) {
+    for (URIish uri :
+        getURIs(new Project.NameKey(event.getProjectName()), FilterType.PROJECT_DELETION)) {
       deleteProject(uri);
     }
   }
 
   @Override
   public void onHeadUpdated(HeadUpdatedListener.Event event) {
-    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()),
-        FilterType.ALL)) {
+    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), FilterType.ALL)) {
       updateHead(uri, event.getNewHeadName());
     }
   }
 
-  private Set<URIish> getURIs(Project.NameKey projectName,
-      FilterType filterType) {
+  private Set<URIish> getURIs(Project.NameKey projectName, FilterType filterType) {
     if (config.getDestinations(filterType).isEmpty()) {
       return Collections.emptySet();
     }
@@ -200,23 +193,19 @@
         try {
           uri = new URIish(url);
         } catch (URISyntaxException e) {
-          repLog.warn(String.format("adminURL '%s' is invalid: %s", url,
-              e.getMessage()));
+          repLog.warn(String.format("adminURL '%s' is invalid: %s", url, e.getMessage()));
           continue;
         }
 
-        String path = replaceName(uri.getPath(), projectName.get(),
-            config.isSingleProjectMatch());
+        String path = replaceName(uri.getPath(), projectName.get(), config.isSingleProjectMatch());
         if (path == null) {
-          repLog.warn(String
-              .format("adminURL %s does not contain ${name}", uri));
+          repLog.warn(String.format("adminURL %s does not contain ${name}", uri));
           continue;
         }
 
         uri = uri.setPath(path);
         if (!isSSH(uri)) {
-          repLog.warn(String.format(
-              "adminURL '%s' is invalid: only SSH is supported", uri));
+          repLog.warn(String.format("adminURL '%s' is invalid: only SSH is supported", uri));
           continue;
         }
 
@@ -249,9 +238,12 @@
       createRemoteSsh(replicateURI, head);
       repLog.info("Created remote repository: " + replicateURI);
     } else {
-      repLog.warn(String.format("Cannot create new project on remote site %s."
-          + " Only local paths and SSH URLs are supported"
-          + " for remote repository creation", replicateURI));
+      repLog.warn(
+          String.format(
+              "Cannot create new project on remote site %s."
+                  + " Only local paths and SSH URLs are supported"
+                  + " for remote repository creation",
+              replicateURI));
       return false;
     }
     return true;
@@ -267,16 +259,13 @@
         u.link(head);
       }
     } catch (IOException e) {
-      repLog.error(String.format(
-          "Error creating local repository %s:\n", uri.getPath()), e);
+      repLog.error(String.format("Error creating local repository %s:\n", uri.getPath()), e);
     }
   }
 
   private void createRemoteSsh(URIish uri, String head) {
     String quotedPath = QuotedString.BOURNE.quote(uri.getPath());
-    String cmd = "mkdir -p " + quotedPath
-            + " && cd " + quotedPath
-            + " && git init --bare";
+    String cmd = "mkdir -p " + quotedPath + " && cd " + quotedPath + " && git init --bare";
     if (head != null) {
       cmd = cmd + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(head);
     }
@@ -284,12 +273,14 @@
     try {
       executeRemoteSsh(uri, cmd, errStream);
     } catch (IOException e) {
-      repLog.error(String.format(
-             "Error creating remote repository at %s:\n"
-          + "  Exception: %s\n"
-          + "  Command: %s\n"
-          + "  Output: %s",
-          uri, e, cmd, errStream), e);
+      repLog.error(
+          String.format(
+              "Error creating remote repository at %s:\n"
+                  + "  Exception: %s\n"
+                  + "  Command: %s\n"
+                  + "  Output: %s",
+              uri, e, cmd, errStream),
+          e);
     }
   }
 
@@ -301,9 +292,12 @@
       deleteRemoteSsh(replicateURI);
       repLog.info("Deleted remote repository: " + replicateURI);
     } else {
-      repLog.warn(String.format("Cannot delete project on remote site %s."
-          + " Only local paths and SSH URLs are supported"
-          + " for remote repository deletion", replicateURI));
+      repLog.warn(
+          String.format(
+              "Cannot delete project on remote site %s."
+                  + " Only local paths and SSH URLs are supported"
+                  + " for remote repository deletion",
+              replicateURI));
     }
   }
 
@@ -311,9 +305,7 @@
     try {
       recursivelyDelete(new File(uri.getPath()));
     } catch (IOException e) {
-      repLog.error(String.format(
-          "Error deleting local repository %s:\n",
-          uri.getPath()), e);
+      repLog.error(String.format("Error deleting local repository %s:\n", uri.getPath()), e);
     }
   }
 
@@ -342,12 +334,14 @@
     try {
       executeRemoteSsh(uri, cmd, errStream);
     } catch (IOException e) {
-      repLog.error(String.format(
-             "Error deleting remote repository at %s:\n"
-          + "  Exception: %s\n"
-          + "  Command: %s\n"
-          + "  Output: %s",
-          uri, e, cmd, errStream), e);
+      repLog.error(
+          String.format(
+              "Error deleting remote repository at %s:\n"
+                  + "  Exception: %s\n"
+                  + "  Command: %s\n"
+                  + "  Output: %s",
+              uri, e, cmd, errStream),
+          e);
     }
   }
 
@@ -357,27 +351,31 @@
     } else if (isSSH(replicateURI)) {
       updateHeadRemoteSsh(replicateURI, newHead);
     } else {
-      repLog.warn(String.format(
-          "Cannot update HEAD of project on remote site %s."
-              + " Only local paths and SSH URLs are supported"
-              + " for remote HEAD update.", replicateURI));
+      repLog.warn(
+          String.format(
+              "Cannot update HEAD of project on remote site %s."
+                  + " Only local paths and SSH URLs are supported"
+                  + " for remote HEAD update.",
+              replicateURI));
     }
   }
 
   private void updateHeadRemoteSsh(URIish uri, String newHead) {
     String quotedPath = QuotedString.BOURNE.quote(uri.getPath());
-    String cmd = "cd " + quotedPath
-            + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(newHead);
+    String cmd =
+        "cd " + quotedPath + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(newHead);
     OutputStream errStream = newErrorBufferStream();
     try {
       executeRemoteSsh(uri, cmd, errStream);
     } catch (IOException e) {
-      repLog.error(String.format(
-             "Error updating HEAD of remote repository at %s to %s:\n"
-          + "  Exception: %s\n"
-          + "  Command: %s\n"
-          + "  Output: %s",
-          uri, newHead, e, cmd, errStream), e);
+      repLog.error(
+          String.format(
+              "Error updating HEAD of remote repository at %s to %s:\n"
+                  + "  Exception: %s\n"
+                  + "  Command: %s\n"
+                  + "  Output: %s",
+              uri, newHead, e, cmd, errStream),
+          e);
     }
   }
 
@@ -389,20 +387,16 @@
       }
     } catch (IOException e) {
       repLog.error(
-          String.format("Failed to update HEAD of repository %s to %s",
-              uri.getPath(), newHead), e);
+          String.format("Failed to update HEAD of repository %s to %s", uri.getPath(), newHead), e);
     }
   }
 
-  private void executeRemoteSsh(URIish uri, String cmd,
-      OutputStream errStream) throws IOException {
+  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);
+    StreamCopyThread out = new StreamCopyThread(proc.getInputStream(), errStream);
+    StreamCopyThread err = new StreamCopyThread(proc.getErrorStream(), errStream);
     out.start();
     err.start();
     try {
@@ -416,8 +410,7 @@
   }
 
   private RemoteSession connect(URIish uri) throws TransportException {
-    return sshSessionFactoryProvider.get().getSession(uri, null, FS.DETECTED,
-        SSH_REMOTE_TIMEOUT);
+    return sshSessionFactoryProvider.get().getSession(uri, null, FS.DETECTED, SSH_REMOTE_TIMEOUT);
   }
 
   private static OutputStream newErrorBufferStream() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationScheduledEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationScheduledEvent.java
new file mode 100644
index 0000000..7268709
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationScheduledEvent.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2016 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.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.events.RefEvent;
+
+public class ReplicationScheduledEvent extends RefEvent {
+  static final String TYPE = "ref-replication-scheduled";
+
+  final String project;
+  final String ref;
+  final String targetNode;
+
+  public ReplicationScheduledEvent(String project, String ref, String targetNode) {
+    super(TYPE);
+    this.project = project;
+    this.ref = ref;
+    this.targetNode = targetNode;
+  }
+
+  @Override
+  public String getRefName() {
+    return ref;
+  }
+
+  @Override
+  public NameKey getProjectNameKey() {
+    return new Project.NameKey(project);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationSshSessionFactoryProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationSshSessionFactoryProvider.java
index 42bc284..0d1aa06 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationSshSessionFactoryProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationSshSessionFactoryProvider.java
@@ -15,7 +15,6 @@
 package com.googlesource.gerrit.plugins.replication;
 
 import com.google.inject.Provider;
-
 import org.eclipse.jgit.transport.SshSessionFactory;
 
 class ReplicationSshSessionFactoryProvider implements Provider<SshSessionFactory> {
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 27fe841..9a68c83 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
@@ -16,13 +16,11 @@
 
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.Table;
-
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.URIish;
-
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.URIish;
 
 public class ReplicationState {
   private boolean allScheduled;
@@ -46,6 +44,7 @@
       return replicatedNodesCount == nodesToReplicateCount;
     }
   }
+
   private final Table<String, String, RefReplicationStatus> statusByProjectRef;
   private int totalPushTasksCount;
   private int finishedPushTasksCount;
@@ -69,10 +68,13 @@
     return totalPushTasksCount != 0;
   }
 
-  public void notifyRefReplicated(String project, String ref, URIish uri,
-      RefPushResult status, RemoteRefUpdate.Status refUpdateStatus) {
-    pushResultProcessing.onRefReplicatedToOneNode(project, ref, uri, status,
-        refUpdateStatus);
+  public void notifyRefReplicated(
+      String project,
+      String ref,
+      URIish uri,
+      RefPushResult status,
+      RemoteRefUpdate.Status refUpdateStatus) {
+    pushResultProcessing.onRefReplicatedToOneNode(project, ref, uri, status, refUpdateStatus);
 
     RefReplicationStatus completedRefStatus = null;
     boolean allPushTaksCompleted = false;
@@ -122,8 +124,7 @@
   }
 
   /**
-   * Some could be remaining if replication of a ref is completed before all
-   * tasks are scheduled.
+   * Some could be remaining if replication of a ref is completed before all tasks are scheduled.
    */
   private void fireRemainingOnRefReplicatedToAllNodes() {
     for (RefReplicationStatus refStatus : statusByProjectRef.values()) {
@@ -132,8 +133,8 @@
   }
 
   private void doRefPushTasksCompleted(RefReplicationStatus refStatus) {
-    pushResultProcessing.onRefReplicatedToAllNodes(refStatus.project,
-        refStatus.ref, refStatus.nodesToReplicateCount);
+    pushResultProcessing.onRefReplicatedToAllNodes(
+        refStatus.project, refStatus.ref, refStatus.nodesToReplicateCount);
   }
 
   private RefReplicationStatus getRefStatus(String project, String ref) {
@@ -158,19 +159,13 @@
   }
 
   public enum RefPushResult {
-    /**
-     * The ref was not successfully replicated.
-     */
+    /** The ref was not successfully replicated. */
     FAILED,
 
-    /**
-     * The ref is not configured to be replicated.
-     */
+    /** The ref is not configured to be replicated. */
     NOT_ATTEMPTED,
 
-    /**
-     * The ref was successfully replicated.
-     */
+    /** The ref was successfully replicated. */
     SUCCEEDED;
 
     @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateListener.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateListener.java
index 4dcf5a1..8e26906 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateListener.java
@@ -14,16 +14,13 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
-/**
- * Interface for notifying replication status updates.
- */
+/** Interface for notifying replication status updates. */
 public interface ReplicationStateListener {
 
   /**
    * Notify a non-fatal replication error.
    *
-   * Replication states received a non-fatal error with an associated
-   * warning message.
+   * <p>Replication states received a non-fatal error with an associated warning message.
    *
    * @param msg message description of the error
    * @param states replication states impacted
@@ -33,8 +30,7 @@
   /**
    * Notify a fatal replication error.
    *
-   * Replication states have received a fatal error and replication has
-   * failed.
+   * <p>Replication states have received a fatal error and replication has failed.
    *
    * @param msg message description of the error
    * @param states replication states impacted
@@ -44,13 +40,11 @@
   /**
    * Notify a fatal replication error with the associated exception.
    *
-   * Replication states have received a fatal exception and replication has failed.
+   * <p>Replication states have received a fatal exception and replication has failed.
    *
    * @param msg message description of the error
    * @param t exception that caused the replication to fail
    * @param states replication states impacted
    */
-  void error(String msg, Throwable t,
-      ReplicationState... states);
-
+  void error(String msg, Throwable t, ReplicationState... states);
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateLogger.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateLogger.java
index 0a59ad3..cfa95dd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateLogger.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateLogger.java
@@ -20,11 +20,10 @@
 
 /**
  * Wrapper around a Logger that also logs out the replication state.
- * <p>
- * When logging replication errors it is useful to know the current
- * replication state.  This utility class wraps the methods from Logger
- * and logs additional information about the replication state to the
- * stderr console.
+ *
+ * <p>When logging replication errors it is useful to know the current replication state. This
+ * utility class wraps the methods from Logger and logs additional information about the replication
+ * state to the stderr console.
  */
 @Singleton
 class ReplicationStateLogger implements ReplicationStateListener {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
index a10f62f..2b0c16b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
@@ -16,37 +16,32 @@
 
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
-
+import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
 
-import java.io.IOException;
-
 /** Looks up a remote's password in secure.config. */
 class SecureCredentialsFactory implements CredentialsFactory {
   private final Config config;
 
   @Inject
-  SecureCredentialsFactory(SitePaths site)
-      throws ConfigInvalidException, IOException {
+  SecureCredentialsFactory(SitePaths site) throws ConfigInvalidException, IOException {
     config = load(site);
   }
 
-  private static Config load(SitePaths site)
-      throws ConfigInvalidException, IOException {
-    FileBasedConfig cfg =
-        new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
+  private static Config load(SitePaths site) throws ConfigInvalidException, IOException {
+    FileBasedConfig cfg = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
     if (cfg.getFile().exists() && cfg.getFile().length() > 0) {
       try {
         cfg.load();
       } catch (ConfigInvalidException e) {
-        throw new ConfigInvalidException(String.format(
-            "Config file %s is invalid: %s", cfg.getFile(), e.getMessage()), e);
+        throw new ConfigInvalidException(
+            String.format("Config file %s is invalid: %s", cfg.getFile(), e.getMessage()), e);
       } catch (IOException e) {
-        throw new IOException(String.format(
-            "Cannot read %s: %s", cfg.getFile(), e.getMessage()), e);
+        throw new IOException(
+            String.format("Cannot read %s: %s", cfg.getFile(), e.getMessage()), e);
       }
     }
     return cfg;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsProvider.java
index a878ed9..c4294a9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsProvider.java
@@ -49,8 +49,7 @@
   }
 
   @Override
-  public boolean get(URIish uri, CredentialItem... items)
-      throws UnsupportedCredentialItem {
+  public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
     String username = uri.getUser();
     if (username == null) {
       username = cfgUser;
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 be5242e..c701c21 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
@@ -18,23 +18,22 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
-
 import com.googlesource.gerrit.plugins.replication.PushResultProcessing.CommandProcessing;
-
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
 
 @RequiresCapability(StartReplicationCapability.START_REPLICATION)
-@CommandMetaData(name = "start", description = "Start replication for specific project or all projects")
+@CommandMetaData(
+  name = "start",
+  description = "Start replication for specific project or all projects"
+)
 final class StartCommand extends SshCommand {
-  @Inject
-  private ReplicationStateLogger stateLog;
+  @Inject private ReplicationStateLogger stateLog;
 
   @Option(name = "--all", usage = "push all known projects")
   private boolean all;
@@ -42,15 +41,13 @@
   @Option(name = "--url", metaVar = "PATTERN", usage = "pattern to match URL on")
   private String urlMatch;
 
-  @Option(name = "--wait",
-      usage = "wait for replication to finish before exiting")
+  @Option(name = "--wait", usage = "wait for replication to finish before exiting")
   private boolean wait;
 
   @Argument(index = 0, multiValued = true, metaVar = "PATTERN", usage = "project name pattern")
   private List<String> projectPatterns = new ArrayList<>(2);
 
-  @Inject
-  private PushAll.Factory pushFactory;
+  @Inject private PushAll.Factory pushFactory;
 
   @Override
   protected void run() throws Failure {
@@ -76,7 +73,8 @@
         try {
           future.get();
         } catch (InterruptedException e) {
-          stateLog.error("Thread was interrupted while waiting for PushAll operation to finish", e, state);
+          stateLog.error(
+              "Thread was interrupted while waiting for PushAll operation to finish", e, state);
           return;
         } catch (ExecutionException e) {
           stateLog.error("An exception was thrown in PushAll operation", e, state);
diff --git a/src/main/resources/Documentation/cmd-list.md b/src/main/resources/Documentation/cmd-list.md
index 3c6b78f..b6688e0 100644
--- a/src/main/resources/Documentation/cmd-list.md
+++ b/src/main/resources/Documentation/cmd-list.md
@@ -70,5 +70,5 @@
 SEE ALSO
 --------
 
-* [Replication Configuration](config.html)
+* [Replication Configuration](config.md)
 * [Access Control](../../../Documentation/access-control.html)
diff --git a/src/main/resources/Documentation/cmd-start.md b/src/main/resources/Documentation/cmd-start.md
index 19b33ec..59c3d1d 100644
--- a/src/main/resources/Documentation/cmd-start.md
+++ b/src/main/resources/Documentation/cmd-start.md
@@ -134,5 +134,5 @@
 SEE ALSO
 --------
 
-* [Replication Configuration](config.html)
+* [Replication Configuration](config.md)
 * [Access Control](../../../Documentation/access-control.html)
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 0577ab1..b1058b5 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -4,7 +4,7 @@
 Enabling Replication
 --------------------
 
-If replicating over SSH (recommended), ensure the host key of the
+If replicating over SSH, ensure the host key of the
 remote system(s) is already in the Gerrit user's `~/.ssh/known_hosts`
 file.  The easiest way to add the host key is to connect once by hand
 with the command line:
@@ -39,7 +39,7 @@
 ```
 
 To manually trigger replication at runtime, see
-SSH command [start](cmd-start.html).
+SSH command [start](cmd-start.md).
 
 File `replication.config`
 -------------------------
@@ -216,6 +216,17 @@
 
 	By default, 15 seconds.
 
+remote.NAME.rescheduleDelay
+:	Delay when rescheduling a push operation due to an in-flight push
+	running for the same project.
+
+	Cannot be set to a value lower than 3 seconds to avoid a tight loop
+	of schedule/run which could cause 1K+ retries per second.
+
+	A configured value lower than 3 seconds will be rounded to 3 seconds.
+
+	By default, 3 seconds.
+
 remote.NAME.replicationRetry
 :	Time to wait before scheduling a remote push operation previously
 	failed due to an offline remote server.
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 4cbac3a..41829bc 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
@@ -28,19 +28,16 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.gwtorm.server.StandardKeyEncoder;
-
 import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
 import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
-
-import junit.framework.TestCase;
-
+import java.net.URISyntaxException;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.URIish;
-
-import java.net.URISyntaxException;
+import org.junit.Before;
+import org.junit.Test;
 
 @SuppressWarnings("unchecked")
-public class GitUpdateProcessingTest extends TestCase {
+public class GitUpdateProcessingTest {
   static {
     KeyUtil.setEncoderImpl(new StandardKeyEncoder());
   }
@@ -48,9 +45,8 @@
   private EventDispatcher dispatcherMock;
   private GitUpdateProcessing gitUpdateProcessing;
 
-  @Override
-  protected void setUp() throws Exception {
-    super.setUp();
+  @Before
+  public void setUp() throws Exception {
     dispatcherMock = createMock(EventDispatcher.class);
     replay(dispatcherMock);
     ReviewDb reviewDbMock = createNiceMock(ReviewDb.class);
@@ -61,42 +57,58 @@
     gitUpdateProcessing = new GitUpdateProcessing(dispatcherMock);
   }
 
-  public void testHeadRefReplicated() throws URISyntaxException, OrmException {
+  @Test
+  public void headRefReplicated() throws URISyntaxException, OrmException {
     reset(dispatcherMock);
     RefReplicatedEvent expectedEvent =
-        new RefReplicatedEvent("someProject", "refs/heads/master", "someHost",
-            RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+        new RefReplicatedEvent(
+            "someProject",
+            "refs/heads/master",
+            "someHost",
+            RefPushResult.SUCCEEDED,
+            RemoteRefUpdate.Status.OK);
     dispatcherMock.postEvent(RefReplicatedEventEquals.eqEvent(expectedEvent));
     expectLastCall().once();
     replay(dispatcherMock);
 
-    gitUpdateProcessing.onRefReplicatedToOneNode("someProject",
-        "refs/heads/master", new URIish("git://someHost/someProject.git"),
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    gitUpdateProcessing.onRefReplicatedToOneNode(
+        "someProject",
+        "refs/heads/master",
+        new URIish("git://someHost/someProject.git"),
+        RefPushResult.SUCCEEDED,
+        RemoteRefUpdate.Status.OK);
     verify(dispatcherMock);
   }
 
-  public void testChangeRefReplicated() throws URISyntaxException, OrmException {
+  @Test
+  public void changeRefReplicated() throws URISyntaxException, OrmException {
     reset(dispatcherMock);
     RefReplicatedEvent expectedEvent =
-        new RefReplicatedEvent("someProject", "refs/changes/01/1/1", "someHost",
-            RefPushResult.FAILED, RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD);
+        new RefReplicatedEvent(
+            "someProject",
+            "refs/changes/01/1/1",
+            "someHost",
+            RefPushResult.FAILED,
+            RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD);
     dispatcherMock.postEvent(RefReplicatedEventEquals.eqEvent(expectedEvent));
     expectLastCall().once();
     replay(dispatcherMock);
 
-    gitUpdateProcessing.onRefReplicatedToOneNode("someProject",
-        "refs/changes/01/1/1", new URIish("git://someHost/someProject.git"),
-        RefPushResult.FAILED, RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD);
+    gitUpdateProcessing.onRefReplicatedToOneNode(
+        "someProject",
+        "refs/changes/01/1/1",
+        new URIish("git://someHost/someProject.git"),
+        RefPushResult.FAILED,
+        RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD);
     verify(dispatcherMock);
   }
 
-  public void testOnAllNodesReplicated() throws OrmException {
+  @Test
+  public void onAllNodesReplicated() throws OrmException {
     reset(dispatcherMock);
     RefReplicationDoneEvent expectedDoneEvent =
         new RefReplicationDoneEvent("someProject", "refs/heads/master", 5);
-    dispatcherMock.postEvent(
-        RefReplicationDoneEventEquals.eqEvent(expectedDoneEvent));
+    dispatcherMock.postEvent(RefReplicationDoneEventEquals.eqEvent(expectedDoneEvent));
     expectLastCall().once();
     replay(dispatcherMock);
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/PushReplicationTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/PushReplicationTest.java
index 71b6600..8480cbe 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/PushReplicationTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/PushReplicationTest.java
@@ -18,11 +18,10 @@
 import static com.googlesource.gerrit.plugins.replication.Destination.encode;
 import static com.googlesource.gerrit.plugins.replication.Destination.needsUrlEncoding;
 
+import java.net.URISyntaxException;
 import org.eclipse.jgit.transport.URIish;
 import org.junit.Test;
 
-import java.net.URISyntaxException;
-
 public class PushReplicationTest {
 
   @Test
@@ -38,7 +37,7 @@
   }
 
   @Test
-  public void testUrlEncoding() {
+  public void urlEncoding() {
     assertThat(encode("foo/bar/thing")).isEqualTo("foo/bar/thing");
     assertThat(encode("-- All Projects --")).isEqualTo("--%20All%20Projects%20--");
     assertThat(encode("name/with a space")).isEqualTo("name/with%20a%20space");
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java
index d614463..983e97f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java
@@ -35,7 +35,7 @@
     if (!(actual instanceof RefReplicatedEvent)) {
       return false;
     }
-    RefReplicatedEvent actualRefReplicatedEvent = (RefReplicatedEvent)actual;
+    RefReplicatedEvent actualRefReplicatedEvent = (RefReplicatedEvent) actual;
     if (!equals(expected.project, actualRefReplicatedEvent.project)) {
       return false;
     }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java
index 02f96fb..d1284e1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java
@@ -35,7 +35,7 @@
     if (!(actual instanceof RefReplicationDoneEvent)) {
       return false;
     }
-    RefReplicationDoneEvent actualRefReplicatedDoneEvent = (RefReplicationDoneEvent)actual;
+    RefReplicationDoneEvent actualRefReplicatedDoneEvent = (RefReplicationDoneEvent) actual;
     if (!equals(expected.project, actualRefReplicatedDoneEvent.project)) {
       return false;
     }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
index 56096c2..65e2d64 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
@@ -22,14 +22,12 @@
 import static org.junit.Assert.assertEquals;
 
 import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
-
+import java.net.URISyntaxException;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.URIish;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.net.URISyntaxException;
-
 public class ReplicationStateTest {
 
   private ReplicationState replicationState;
@@ -67,41 +65,37 @@
   }
 
   @Test
-  public void shouldFireEventsForReplicationOfOneRefToOneNode()
-      throws URISyntaxException {
+  public void shouldFireEventsForReplicationOfOneRefToOneNode() throws URISyntaxException {
     resetToDefault(pushResultProcessingMock);
     URIish uri = new URIish("git://someHost/someRepo.git");
 
     //expected events
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
-        uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject",
-        "someRef", 1);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "someRef", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject", "someRef", 1);
     pushResultProcessingMock.onAllRefsReplicatedToAllNodes(1);
     replay(pushResultProcessingMock);
 
     //actual test
     replicationState.increasePushTaskCount("someProject", "someRef");
     replicationState.markAllPushTasksScheduled();
-    replicationState.notifyRefReplicated("someProject", "someRef", uri,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "someRef", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
     verify(pushResultProcessingMock);
   }
 
   @Test
-  public void shouldFireEventsForReplicationOfOneRefToMultipleNodes()
-      throws URISyntaxException {
+  public void shouldFireEventsForReplicationOfOneRefToMultipleNodes() throws URISyntaxException {
     resetToDefault(pushResultProcessingMock);
     URIish uri1 = new URIish("git://someHost1/someRepo.git");
     URIish uri2 = new URIish("git://someHost2/someRepo.git");
 
     //expected events
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
-        uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
-        uri2, RefPushResult.FAILED, RemoteRefUpdate.Status.NON_EXISTING);
-    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject",
-        "someRef", 2);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "someRef", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "someRef", uri2, RefPushResult.FAILED, RemoteRefUpdate.Status.NON_EXISTING);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject", "someRef", 2);
     pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
     replay(pushResultProcessingMock);
 
@@ -109,10 +103,10 @@
     replicationState.increasePushTaskCount("someProject", "someRef");
     replicationState.increasePushTaskCount("someProject", "someRef");
     replicationState.markAllPushTasksScheduled();
-    replicationState.notifyRefReplicated("someProject", "someRef", uri1,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    replicationState.notifyRefReplicated("someProject", "someRef", uri2,
-        RefPushResult.FAILED, RemoteRefUpdate.Status.NON_EXISTING);
+    replicationState.notifyRefReplicated(
+        "someProject", "someRef", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "someRef", uri2, RefPushResult.FAILED, RemoteRefUpdate.Status.NON_EXISTING);
     verify(pushResultProcessingMock);
   }
 
@@ -125,20 +119,18 @@
     URIish uri3 = new URIish("git://host3/someRepo.git");
 
     //expected events
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
-        uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
-        uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
-        uri3, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
-        uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
-        uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock
-        .onRefReplicatedToAllNodes("someProject", "ref1", 3);
-    pushResultProcessingMock
-        .onRefReplicatedToAllNodes("someProject", "ref2", 2);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "ref1", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "ref1", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "ref1", uri3, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "ref2", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject", "ref1", 3);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject", "ref2", 2);
     pushResultProcessingMock.onAllRefsReplicatedToAllNodes(5);
     replay(pushResultProcessingMock);
 
@@ -149,30 +141,29 @@
     replicationState.increasePushTaskCount("someProject", "ref2");
     replicationState.increasePushTaskCount("someProject", "ref2");
     replicationState.markAllPushTasksScheduled();
-    replicationState.notifyRefReplicated("someProject", "ref1", uri1,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    replicationState.notifyRefReplicated("someProject", "ref1", uri2,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    replicationState.notifyRefReplicated("someProject", "ref1", uri3,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    replicationState.notifyRefReplicated("someProject", "ref2", uri1,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    replicationState.notifyRefReplicated("someProject", "ref2", uri2,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "ref1", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "ref1", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "ref1", uri3, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "ref2", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
     verify(pushResultProcessingMock);
   }
 
   @Test
-  public void shouldFireEventsForReplicationSameRefDifferentProjects()
-      throws URISyntaxException {
+  public void shouldFireEventsForReplicationSameRefDifferentProjects() throws URISyntaxException {
     resetToDefault(pushResultProcessingMock);
     URIish uri = new URIish("git://host1/someRepo.git");
 
     //expected events
-    pushResultProcessingMock.onRefReplicatedToOneNode("project1", "ref1", uri,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToOneNode("project2", "ref2", uri,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "project1", "ref1", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "project2", "ref2", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
     pushResultProcessingMock.onRefReplicatedToAllNodes("project1", "ref1", 1);
     pushResultProcessingMock.onRefReplicatedToAllNodes("project2", "ref2", 1);
     pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
@@ -182,10 +173,10 @@
     replicationState.increasePushTaskCount("project1", "ref1");
     replicationState.increasePushTaskCount("project2", "ref2");
     replicationState.markAllPushTasksScheduled();
-    replicationState.notifyRefReplicated("project1", "ref1", uri,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    replicationState.notifyRefReplicated("project2", "ref2", uri,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "project1", "ref1", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "project2", "ref2", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
     verify(pushResultProcessingMock);
   }
 
@@ -195,25 +186,23 @@
     resetToDefault(pushResultProcessingMock);
     URIish uri1 = new URIish("git://host1/someRepo.git");
 
-   //expected events
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
-        uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
-        uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    pushResultProcessingMock
-        .onRefReplicatedToAllNodes("someProject", "ref1", 1);
-    pushResultProcessingMock
-        .onRefReplicatedToAllNodes("someProject", "ref2", 1);
+    //expected events
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "ref1", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToOneNode(
+        "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject", "ref1", 1);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject", "ref2", 1);
     pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
     replay(pushResultProcessingMock);
 
     //actual test
     replicationState.increasePushTaskCount("someProject", "ref1");
     replicationState.increasePushTaskCount("someProject", "ref2");
-    replicationState.notifyRefReplicated("someProject", "ref1", uri1,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
-    replicationState.notifyRefReplicated("someProject", "ref2", uri1,
-        RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "ref1", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
+    replicationState.notifyRefReplicated(
+        "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK);
     replicationState.markAllPushTasksScheduled();
     verify(pushResultProcessingMock);
   }