summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdwin Kempin <edwin.kempin@sap.com>2013-09-11 16:39:05 +0200
committerOswald Buddenhagen <oswald.buddenhagen@theqtcompany.com>2015-02-10 21:05:24 +0000
commit9d8d29e97b2ad94a2684b0956a15506836e8eff3 (patch)
tree095d716034fe27abc084ddb160454c09f5726c99
parent70371bc583fa0387e66ec42d6504641e2f5d7779 (diff)
Support replication of project deletions
The replication plugin can now be configured to listen to project deletion events and to replicate the project deletions. In the configuration it can be decided per remote destination whether project deletions should be replicated to it or not. By default project deletions are not replicated. Change-Id: I1207015ed361155dd0353bed86860c794cdb2517 Signed-off-by: Edwin Kempin <edwin.kempin@sap.com> (cherry picked from commit ad923e4e1e4ae3923d15a8b4eee36379423846be) Reviewed-by: Ismo Haataja <ismo.haataja@digia.com> Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@theqtcompany.com>
-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