summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java7
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java4
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java124
-rw-r--r--src/main/resources/Documentation/config.md6
4 files changed, 120 insertions, 21 deletions
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 1054aed..19a9359 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -83,6 +83,7 @@ class Destination {
private final GitRepositoryManager gitManager;
private final boolean createMissingRepos;
private final boolean replicatePermissions;
+ private final boolean replicateProjectDeletions;
private final String remoteNameStyle;
private volatile WorkQueue.Executor pool;
private final PerThreadRequestScope.Scoper threadScoper;
@@ -111,6 +112,8 @@ class Destination {
cfg.getBoolean("remote", rc.getName(), "createMissingRepositories", true);
replicatePermissions =
cfg.getBoolean("remote", rc.getName(), "replicatePermissions", true);
+ replicateProjectDeletions =
+ cfg.getBoolean("remote", rc.getName(), "replicateProjectDeletions", false);
remoteNameStyle = Objects.firstNonNull(
cfg.getString("remote", rc.getName(), "remoteNameStyle"), "slash");
projects = cfg.getStringList("remote", rc.getName(), "projects");
@@ -449,6 +452,10 @@ class Destination {
return replicatePermissions;
}
+ boolean isReplicateProjectDeletions() {
+ return replicateProjectDeletions;
+ }
+
List<URIish> getURIs(Project.NameKey project, String urlMatch) {
List<URIish> r = Lists.newArrayListWithCapacity(remote.getURIs().size());
for (URIish uri : remote.getURIs()) {
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 974cf4a..bc52db8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -17,6 +17,7 @@ package com.googlesource.gerrit.plugins.replication;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
@@ -34,6 +35,9 @@ class ReplicationModule extends AbstractModule {
DynamicSet.bind(binder(), NewProjectCreatedListener.class)
.to(ReplicationQueue.class);
+ DynamicSet.bind(binder(), ProjectDeletedListener.class)
+ .to(ReplicationQueue.class);
+
bind(OnStartStop.class).in(Scopes.SINGLETON);
bind(LifecycleListener.class)
.annotatedWith(UniqueAnnotations.create())
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 502994a..83f7847 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -17,9 +17,11 @@ package com.googlesource.gerrit.plugins.replication;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.PluginUser;
@@ -61,7 +63,8 @@ import java.util.Set;
class ReplicationQueue implements
LifecycleListener,
GitReferenceUpdatedListener,
- NewProjectCreatedListener {
+ NewProjectCreatedListener,
+ ProjectDeletedListener {
static final Logger log = LoggerFactory.getLogger(ReplicationQueue.class);
private static final WrappedLogger wrappedLog = new WrappedLogger(log);
@@ -246,19 +249,36 @@ class ReplicationQueue implements
@Override
public void onNewProjectCreated(NewProjectCreatedListener.Event event) {
+ for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), false)) {
+ createProject(uri, event.getHeadName());
+ }
+ }
+
+ @Override
+ public void onProjectDeleted(ProjectDeletedListener.Event event) {
+ for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), true)) {
+ deleteProject(uri);
+ }
+ }
+
+ private Set<URIish> getURIs(Project.NameKey projectName,
+ boolean forProjectDeletion) {
if (configs.isEmpty()) {
- return;
+ return Collections.emptySet();
}
if (!running) {
log.error("Replication plugin did not finish startup before event");
- return;
+ return Collections.emptySet();
}
- Project.NameKey projectName = new Project.NameKey(event.getProjectName());
+ Set<URIish> uris = Sets.newHashSet();
for (Destination config : configs) {
if (!config.wouldPushProject(projectName)) {
continue;
}
+ if (forProjectDeletion && !config.isReplicateProjectDeletions()) {
+ continue;
+ }
List<URIish> uriList = config.getURIs(projectName, "*");
String[] adminUrls = config.getAdminUrls();
boolean adminURLUsed = false;
@@ -289,16 +309,17 @@ class ReplicationQueue implements
continue;
}
- createProject(uri, event.getHeadName());
+ uris.add(uri);
adminURLUsed = true;
}
if (!adminURLUsed) {
for (URIish uri : uriList) {
- createProject(uri, event.getHeadName());
+ uris.add(uri);
}
}
}
+ return uris;
}
private void createProject(URIish replicateURI, String head) {
@@ -342,21 +363,7 @@ class ReplicationQueue implements
}
OutputStream errStream = newErrorBufferStream();
try {
- 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();
+ executeRemotSsh(uri, cmd, errStream);
} catch (IOException e) {
log.error(String.format(
"Error creating remote repository at %s:\n"
@@ -367,6 +374,81 @@ class ReplicationQueue implements
}
}
+ private void deleteProject(URIish replicateURI) {
+ if (!replicateURI.isRemote()) {
+ deleteLocally(replicateURI);
+ } else if (isSSH(replicateURI)) {
+ deleteRemoteSsh(replicateURI);
+ } else {
+ log.warn(String.format("Cannot delete project on remote site %s."
+ + " Only local paths and SSH URLs are supported"
+ + " for remote repository deletion", replicateURI));
+ }
+ }
+
+ private static void deleteLocally(URIish uri) {
+ try {
+ recursivelyDelete(new File(uri.getPath()));
+ } catch (IOException e) {
+ log.error(String.format("Failed to delete repository %s", uri.getPath()), e);
+ }
+ }
+
+ public static void recursivelyDelete(File dir) throws IOException {
+ File[] contents = dir.listFiles();
+ if (contents != null) {
+ for (File d : contents) {
+ if (d.isDirectory()) {
+ recursivelyDelete(d);
+ } else {
+ if (!d.delete()) {
+ throw new IOException("Failed to delete: " + d.getAbsolutePath());
+ }
+ }
+ }
+ }
+ if (!dir.delete()) {
+ throw new IOException("Failed to delete: " + dir.getAbsolutePath());
+ }
+ }
+
+ private static void deleteRemoteSsh(URIish uri) {
+ String quotedPath = QuotedString.BOURNE.quote(uri.getPath());
+ String cmd = "rm -rf " + quotedPath;
+ OutputStream errStream = newErrorBufferStream();
+ try {
+ executeRemotSsh(uri, cmd, errStream);
+ } catch (IOException e) {
+ log.error(String.format(
+ "Error deleting remote repository at %s:\n"
+ + " Exception: %s\n"
+ + " Command: %s\n"
+ + " Output: %s",
+ uri, e, cmd, errStream), e);
+ }
+ }
+
+ private static void executeRemotSsh(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 static RemoteSession connect(URIish uri) throws TransportException {
return SshSessionFactory.getInstance().getSession(uri, null, FS.DETECTED, 0);
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index e22409a..9c114cf 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -223,6 +223,12 @@ remote.NAME.replicatePermissions
By default, true, replicating everything.
+remote.NAME.replicateProjectDeletions
+: If true, project deletions will also be replicated to the
+ remote site.
+
+ By default, false, do *not* replicate project deletions.
+
remote.NAME.mirror
: If true, replication will remove remote branches that absent
locally or invisible to the replication (for example read