diff options
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 |