Merge branch 'stable-3.1' into stable-3.2

* stable-3.1:
  Add Gatling e2e-test for rename-project
  Add REST endpoint to rename operation
  Adapt checks to the new rename replication feature
  Introduce rename replication feature
  Upgrade bazlets to latest stable-3.1 to build with 3.1.12 API
  RenamePreconditions: check if project state is not null
  Upgrade bazlets to latest stable-3.0 to build with 3.0.15 API
  Upgrade bazlets to latest stable-2.16 to build with 2.16.26 API
  Upgrade bazlets to latest stable-2.16 to build with 2.16.23 API

Conflict resolution for WORKSPACE is to keep the original bazlets
version.

A new testRenameReplicationViaSshAdminUser test needs to be adapted to
stable-3.2 because ProjectCahce.get() method return Optional of
ProjectState instead. GerritConfig is moved from acceptance package
to acceptance.config.

Change-Id: If8b67dd7bca810db1e4d31e5a8d16c5efb7d1f1e
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java
index ec87187..1a3570d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java
@@ -14,24 +14,56 @@
 
 package com.googlesource.gerrit.plugins.renameproject;
 
+import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 @Singleton
 public class Configuration {
+  private static final int DEFAULT_SSH_CONNECTION_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
+  private static final String URL_KEY = "url";
 
   private final int indexThreads;
+  private final int sshCommandTimeout;
+  private final int sshConnectionTimeout;
+
+  private final Set<String> urls;
 
   @Inject
   public Configuration(PluginConfigFactory pluginConfigFactory, @PluginName String pluginName) {
     PluginConfig cfg = pluginConfigFactory.getFromGerritConfig(pluginName);
     indexThreads = cfg.getInt("indexThreads", 4);
+    sshCommandTimeout = cfg.getInt("sshCommandTimeout", 0);
+    sshConnectionTimeout = cfg.getInt("sshConnectionTimeout", DEFAULT_SSH_CONNECTION_TIMEOUT_MS);
+
+    urls =
+        Arrays.stream(cfg.getStringList(URL_KEY))
+            .filter(Objects::nonNull)
+            .filter(s -> !s.isEmpty())
+            .map(s -> CharMatcher.is('/').trimTrailingFrom(s))
+            .collect(Collectors.toSet());
   }
 
   public int getIndexThreads() {
     return indexThreads;
   }
+
+  public Set<String> getUrls() {
+    return urls;
+  }
+
+  public int getSshCommandTimeout() {
+    return sshCommandTimeout;
+  }
+
+  public int getSshConnectionTimeout() {
+    return sshConnectionTimeout;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
index 568a7be..f35c55e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
@@ -29,6 +29,7 @@
 import com.googlesource.gerrit.plugins.renameproject.database.DatabaseRenameHandler;
 import com.googlesource.gerrit.plugins.renameproject.database.IndexUpdateHandler;
 import com.googlesource.gerrit.plugins.renameproject.fs.FilesystemRenameHandler;
+import org.eclipse.jgit.transport.SshSessionFactory;
 
 public class Module extends AbstractModule {
 
@@ -47,6 +48,7 @@
     bind(RenamePreconditions.class);
     bind(IndexUpdateHandler.class);
     bind(RevertRenameProject.class);
+    bind(SshSessionFactory.class).toProvider(RenameReplicationSshSessionFactoryProvider.class);
 
     install(
         new RestApiModule() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java
index 6a80283..cac0b30 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java
@@ -18,6 +18,8 @@
 import static com.googlesource.gerrit.plugins.renameproject.RenameProject.WARNING_LIMIT;
 
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.ProjectResource;
@@ -35,6 +37,7 @@
 import java.util.NoSuchElementException;
 import java.util.Optional;
 import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,6 +49,9 @@
   @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "new name for the project")
   private String newProjectName;
 
+  @Option(name = "--replication", usage = "perform only file system rename")
+  private boolean replication;
+
   private static final Logger log = LoggerFactory.getLogger(RenameCommand.class);
   private final RenameProject renameProject;
   private final Provider<CurrentUser> self;
@@ -62,15 +68,25 @@
       RenameProject.Input input = new RenameProject.Input();
       input.name = newProjectName;
       ProjectResource rsrc = new ProjectResource(projectState, self.get());
-      try (CommandProgressMonitor monitor = new CommandProgressMonitor(stdout)) {
-        renameProject.assertCanRename(rsrc, input, Optional.of(monitor));
-        List<Change.Id> changeIds = renameProject.getChanges(rsrc, Optional.of(monitor));
-        if (continueRename(changeIds, monitor)) {
-          renameProject.doRename(changeIds, rsrc, input, Optional.of(monitor));
+
+      if (replication) {
+        if (renameProject.isAdmin()) {
+          renameProject.fsRenameStep(
+              rsrc.getNameKey(), Project.nameKey(newProjectName), Optional.empty());
         } else {
-          log.debug(CANCELLATION_MSG);
-          stdout.println(CANCELLATION_MSG);
-          stdout.flush();
+          throw new AuthException("Not allowed to replicate rename");
+        }
+      } else {
+        try (CommandProgressMonitor monitor = new CommandProgressMonitor(stdout)) {
+          renameProject.assertCanRename(rsrc, input, Optional.of(monitor));
+          List<Change.Id> changeIds = renameProject.getChanges(rsrc, Optional.of(monitor));
+          if (continueRename(changeIds, monitor)) {
+            renameProject.doRename(changeIds, rsrc, input, Optional.of(monitor));
+          } else {
+            log.debug(CANCELLATION_MSG);
+            stdout.println(CANCELLATION_MSG);
+            stdout.flush();
+          }
         }
       }
     } catch (NoSuchElementException | RestApiException | IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
index 3104491..f4a5418 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
@@ -50,10 +50,13 @@
 import com.googlesource.gerrit.plugins.renameproject.fs.FilesystemRenameHandler;
 import com.googlesource.gerrit.plugins.renameproject.monitor.ProgressMonitor;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.transport.URIish;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -77,6 +80,7 @@
   }
 
   static class Input {
+
     String name;
     boolean continueWithRename;
   }
@@ -101,6 +105,8 @@
   private final PermissionBackend permissionBackend;
   private final Cache<Change.Id, String> changeIdProjectCache;
   private final RevertRenameProject revertRenameProject;
+  private final SshHelper sshHelper;
+  private final Configuration cfg;
 
   private List<Step> stepsPerformed;
 
@@ -118,7 +124,9 @@
       RenameLog renameLog,
       PermissionBackend permissionBackend,
       @Named(CACHE_NAME) Cache<Change.Id, String> changeIdProjectCache,
-      RevertRenameProject revertRenameProject) {
+      RevertRenameProject revertRenameProject,
+      SshHelper sshHelper,
+      Configuration cfg) {
     this.dbHandler = dbHandler;
     this.fsHandler = fsHandler;
     this.cacheHandler = cacheHandler;
@@ -132,6 +140,8 @@
     this.permissionBackend = permissionBackend;
     this.changeIdProjectCache = changeIdProjectCache;
     this.revertRenameProject = revertRenameProject;
+    this.sshHelper = sshHelper;
+    this.cfg = cfg;
     this.stepsPerformed = new ArrayList<>();
   }
 
@@ -147,14 +157,22 @@
     }
   }
 
+  private PermissionBackend.WithUser getUserPermissions() {
+    return permissionBackend.user(userProvider.get());
+  }
+
   protected boolean canRename(ProjectResource rsrc) {
-    PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider.get());
-    return userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
+    PermissionBackend.WithUser userPermission = getUserPermissions();
+    return isAdmin()
         || userPermission.testOrFalse(new PluginPermission(pluginName, RENAME_PROJECT))
         || (userPermission.testOrFalse(new PluginPermission(pluginName, RENAME_OWN_PROJECT))
             && isOwner(rsrc));
   }
 
+  boolean isAdmin() {
+    return getUserPermissions().testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
+  }
+
   private boolean isOwner(ProjectResource project) {
     try {
       permissionBackend
@@ -210,6 +228,9 @@
       changeIdProjectCache.invalidateAll(changeIds);
 
       pluginEvent.fire(pluginName, pluginName, oldProjectKey.get() + ":" + newProjectKey.get());
+
+      // replicate rename-project operation to other replica instances
+      replicateRename(sshHelper, input, oldProjectKey);
     } catch (Exception e) {
       if (stepsPerformed.isEmpty()) {
         log.error("Renaming procedure failed. Exception caught: {}", e.toString());
@@ -307,4 +328,22 @@
     Project.NameKey oldProjectKey = rsrc.getNameKey();
     return dbHandler.getChangeIds(oldProjectKey);
   }
+
+  void replicateRename(SshHelper sshHelper, Input input, Project.NameKey oldProjectKey) {
+    for (String url : cfg.getUrls()) {
+      try {
+        OutputStream errStream = sshHelper.newErrorBufferStream();
+        sshHelper.executeRemoteSsh(
+            new URIish(url),
+            pluginName + " " + oldProjectKey.get() + " " + input.name + " --replication",
+            errStream);
+        String errorMessage = errStream.toString();
+        if (!errorMessage.isEmpty()) {
+          throw new RenameReplicationException(errorMessage);
+        }
+      } catch (IOException | URISyntaxException | RenameReplicationException e) {
+        log.error("Failed to replicate rename to {}: {}", url, e.getMessage());
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameReplicationException.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameReplicationException.java
new file mode 100644
index 0000000..0df5f29
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameReplicationException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2021 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.renameproject;
+
+/**
+ * Thrown when trying to replicate rename to a replica and encountered an error on the replica side.
+ */
+public class RenameReplicationException extends Exception {
+
+  private static final long serialVersionUID = 1L;
+
+  public RenameReplicationException(String message) {
+    super("Exception during rename replication: " + message);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameReplicationSshSessionFactoryProvider.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameReplicationSshSessionFactoryProvider.java
new file mode 100644
index 0000000..bab2b46
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameReplicationSshSessionFactoryProvider.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2021 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.renameproject;
+
+import com.google.inject.Provider;
+import org.eclipse.jgit.transport.SshSessionFactory;
+
+class RenameReplicationSshSessionFactoryProvider implements Provider<SshSessionFactory> {
+
+  @Override
+  public SshSessionFactory get() {
+    return SshSessionFactory.getInstance();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/SshHelper.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/SshHelper.java
new file mode 100644
index 0000000..ae3ec9f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/SshHelper.java
@@ -0,0 +1,93 @@
+// 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.renameproject;
+
+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;
+
+/** Utility class that provides SSH access to remote URIs. */
+public class SshHelper {
+  private final Provider<SshSessionFactory> sshSessionFactoryProvider;
+  private final int commandTimeout;
+  private final int connectionTimeout;
+
+  @Inject
+  protected SshHelper(
+      Configuration replicationConfig, Provider<SshSessionFactory> sshSessionFactoryProvider) {
+    this.sshSessionFactoryProvider = sshSessionFactoryProvider;
+    this.commandTimeout = replicationConfig.getSshCommandTimeout();
+    this.connectionTimeout = replicationConfig.getSshConnectionTimeout();
+  }
+
+  public int executeRemoteSsh(URIish uri, String cmd, OutputStream errStream) throws IOException {
+    RemoteSession ssh = connect(uri);
+    Process proc = ssh.exec(cmd, commandTimeout);
+    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();
+  }
+
+  public 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);
+        }
+      }
+    };
+  }
+
+  protected RemoteSession connect(URIish uri) throws TransportException {
+    return sshSessionFactoryProvider.get().getSession(uri, null, FS.DETECTED, connectionTimeout);
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index da04a05..e0bd0b6 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -35,14 +35,24 @@
 Replication of project renaming
 -------------------------------
 
-This plugin does not replicate any project renamings itself, but it triggers
-an event when a project is renamed. The [replication plugin]
-(https://gerrit-review.googlesource.com/#/admin/projects/plugins/replication)
-is configured to listen to the event of type `PluginEvent`, which provides
-replication plugin with the required information in order to replicate the
-project rename functionality. `Rename-project` does not provide any custom
-event. Instead, it uses the existing `PluginEvent` which allows communication
-between two plugins directly.
+This plugin can replicate project renaming by itself, if `gerrit.config` has a `url` entry at the
+plugin configuration section and if master and all other replicas have this plugin installed. Once
+configured, replication of rename will start on every successful renaming of a local project. When
+the plugin completes the renaming operation on the master instance successfully, it sends an ssh
+command to replicas' rename-project plugin using the hostname provided in the configuration file.
+Replicas then perform their own local file system rename.
+
+The caveat is if ssh rename replication fails, the plugin doesn't retry the rename replication
+operation. This results in primary and replica instances being out of sync. The admin then will have
+to consider the following steps:
+
+1. Start replication on the renamed project from the primary side. For more information, see the
+[replication start command](../../replication/Documentation/cmd-start.md).
+
+2. Confirm that the renamed project was replicated.
+
+3. Delete the original (not renamed) project repository directory contents on the replica side; this
+step is optional.
 
 Access
 ------
@@ -54,3 +64,6 @@
 is granted the 'Rename Own Project' capability (provided by this
 plugin). However, because of all the caveats of this plugin, it is not
 recommended to delegate the 'Rename Project' capability to any non-admin user.
+
+As previously described, this plugin is capable of performing only file system rename, if a user is
+an admin user and if the `--replication` option is used.
diff --git a/src/main/resources/Documentation/cmd-rename.md b/src/main/resources/Documentation/cmd-rename.md
index 08c6974..0f54eb6 100644
--- a/src/main/resources/Documentation/cmd-rename.md
+++ b/src/main/resources/Documentation/cmd-rename.md
@@ -11,6 +11,7 @@
 ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@
   <PROJECT>
   <NEWNAME>
+  [--replication]
 ```
 
 DESCRIPTION
@@ -28,6 +29,15 @@
 ---------
 This command is intended to be used in scripts.
 
+
+OPTIONS
+-------
+`--replication`
+:   To perform only file system rename. This option is used for replication of
+    rename operation to other replica instances. This command should not be used
+    towards non-replica instances. This option requires the user to have admin
+    permissions.
+
 EXAMPLES
 --------
 
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 3d5b26d..7a5faeb 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -13,3 +13,33 @@
   [plugin "@PLUGIN@"]
     indexThreads = 4
 ```
+
+Rename project replication is enabled by adding appropriate `url`'s.
+For example:
+
+```
+  [plugin "@PLUGIN@"]
+    url = ssh://admin@mirror1.us.some.org
+    url = ssh://mirror2.us.some.org:29418
+```
+
+To specify the port number, it is required to put the `ssh://` prefix followed by hostname and then
+port number after `:`. It is also possible to specify the ssh user by passing `USERNAME@` as a
+prefix for hostname.
+
+Rename replication is done over SSH, so 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:
+
+```
+  sudo su -c 'ssh mirror1.us.some.org echo' gerrit2
+```
+
+@PLUGIN@ plugin uses the ssh rename command towards the replica(s) with `--replication` option to
+replicate the rename operation. It is possible to customize the parameters of the underlying ssh
+client doing these calls by specifying the following fields:
+
+* `sshCommandTimeout` : Timeout for SSH command execution. If 0, there is no timeout, and
+the client waits indefinitely. By default, 0.
+* `sshConnectionTimeout` : Timeout for SSH connections in minutes. If 0, there is no timeout, and
+the client waits indefinitely. By default, 2 minutes.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
index 967f5eb..7b41fb2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
@@ -15,6 +15,11 @@
 package com.googlesource.gerrit.plugins.renameproject;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.common.cache.Cache;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
@@ -23,6 +28,7 @@
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.Change.Id;
 import com.google.gerrit.entities.Project;
@@ -31,10 +37,12 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.renameproject.RenameProject.Input;
+import java.io.OutputStream;
 import java.util.List;
 import java.util.Optional;
 import javax.inject.Named;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.transport.URIish;
 import org.junit.Test;
 
 @TestPlugin(
@@ -46,7 +54,10 @@
 
   private static final String PLUGIN_NAME = "rename-project";
   private static final String NEW_PROJECT_NAME = "newProject";
+  private static final String NON_EXISTING_NAME = "nonExistingProject";
   private static final String CACHE_NAME = "changeid_project";
+  private static final String REPLICATION_OPTION = "--replication";
+  private static final String URL = "ssh://localhost:29418";
 
   @Inject private RequestScopeOperations requestScopeOperations;
 
@@ -69,6 +80,29 @@
 
   @Test
   @UseLocalDisk
+  public void testRenameReplicationViaSshNotAdminUser() throws Exception {
+    createChange();
+    userSshSession.exec(
+        PLUGIN_NAME + " " + project.get() + " " + NEW_PROJECT_NAME + " " + REPLICATION_OPTION);
+
+    userSshSession.assertFailure();
+    assertThat(userSshSession.getError()).contains("Not allowed to replicate rename");
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRenameReplicationViaSshAdminUser() throws Exception {
+    createChange();
+    adminSshSession.exec(
+        PLUGIN_NAME + " " + project.get() + " " + NEW_PROJECT_NAME + " " + REPLICATION_OPTION);
+
+    adminSshSession.assertSuccess();
+    Optional<ProjectState> projectState = projectCache.get(Project.nameKey(NEW_PROJECT_NAME));
+    assertThat(projectState.isPresent()).isTrue();
+  }
+
+  @Test
+  @UseLocalDisk
   public void testRenameViaSshWithEmptyNewName() throws Exception {
     createChange();
     String newProjectName = "";
@@ -100,6 +134,27 @@
 
   @Test
   @UseLocalDisk
+  public void testRenameReplicationViaSshOnNonExisting() throws Exception {
+    createChange();
+    adminSshSession.exec(
+        PLUGIN_NAME + " " + NON_EXISTING_NAME + " " + project.get() + " " + REPLICATION_OPTION);
+
+    assertThat(adminSshSession.getError()).contains("project " + NON_EXISTING_NAME + " not found");
+    adminSshSession.assertFailure();
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRenameNonExistingProjectFail() throws Exception {
+    createChange();
+    adminSshSession.exec(PLUGIN_NAME + " " + NON_EXISTING_NAME + " " + project.get());
+
+    assertThat(adminSshSession.getError()).contains("project " + NON_EXISTING_NAME + " not found");
+    adminSshSession.assertFailure();
+  }
+
+  @Test
+  @UseLocalDisk
   public void testRenameSubscribedFail() throws Exception {
     NameKey superProject = createProjectOverAPI("super-project", null, true, null);
     TestRepository<?> superRepo = cloneProject(superProject);
@@ -155,6 +210,27 @@
 
   @Test
   @UseLocalDisk
+  @GerritConfig(name = "plugin.rename-project.url", value = URL)
+  public void testReplicateRename() throws Exception {
+    RenameProject renameProject = plugin.getSysInjector().getInstance(RenameProject.class);
+    SshHelper sshHelper = mock(SshHelper.class);
+    OutputStream errStream = mock(OutputStream.class);
+
+    Input input = new Input();
+    input.name = NEW_PROJECT_NAME;
+    String expectedCommand =
+        PLUGIN_NAME + " " + project.get() + " " + NEW_PROJECT_NAME + " " + REPLICATION_OPTION;
+
+    when(sshHelper.newErrorBufferStream()).thenReturn(errStream);
+    when(errStream.toString()).thenReturn("");
+
+    renameProject.replicateRename(sshHelper, input, project);
+    verify(sshHelper, atLeastOnce())
+        .executeRemoteSsh(eq(new URIish(URL)), eq(expectedCommand), eq(errStream));
+  }
+
+  @Test
+  @UseLocalDisk
   public void testRenameViaHttpSuccessful() throws Exception {
     createChange();
     RestResponse r = renameProjectTo(NEW_PROJECT_NAME);