Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Document how to exclude projects from replication

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