Merge branch 'stable-2.10' into stable-2.11

* stable-2.10:
  Fix replication_log with external log4j.configuration

Change-Id: I894ad73610106fe6e2b4d56d6b1749546e14c3b8
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
index 83b97c6..62cad2c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
@@ -22,8 +22,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 
-import com.googlesource.gerrit.plugins.replication.RemoteSiteUser;
-
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -75,9 +73,9 @@
   }
 
   @Override
-  public synchronized List<Destination> getDestinations() {
+  public synchronized List<Destination> getDestinations(FilterType filterType) {
     reloadIfNeeded();
-    return currentConfig.getDestinations();
+    return currentConfig.getDestinations(filterType);
   }
 
   private void reloadIfNeeded() {
@@ -91,7 +89,7 @@
         this.currentConfig = newConfig;
         this.currentConfigTs = currentConfig.getCfgPath().lastModified();
         log.info("Configuration reloaded: "
-            + currentConfig.getDestinations().size() + " destinations, "
+            + currentConfig.getDestinations(FilterType.ALL).size() + " destinations, "
             + discarded + " replication events discarded");
 
       } catch (Exception e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
index 9d6ce69..4017822 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
@@ -42,8 +42,7 @@
     this.site = site;
     this.config = config;
     this.secureCredentialsFactory =
-        new AtomicReference<SecureCredentialsFactory>(
-            new SecureCredentialsFactory(site));
+        new AtomicReference<>(new SecureCredentialsFactory(site));
     this.secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
   }
 
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 ef659f2..a175285 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -14,11 +14,10 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -57,6 +56,7 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -78,8 +78,8 @@
   private final int retryDelay;
   private final Object stateLock = new Object();
   private final int lockErrorMaxRetries;
-  private final Map<URIish, PushOne> pending = new HashMap<URIish, PushOne>();
-  private final Map<URIish, PushOne> inFlight = new HashMap<URIish, PushOne>();
+  private final Map<URIish, PushOne> pending = new HashMap<>();
+  private final Map<URIish, PushOne> inFlight = new HashMap<>();
   private final PushOne.Factory opFactory;
   private final ProjectControl.Factory projectControlFactory;
   private final GitRepositoryManager gitManager;
@@ -116,7 +116,7 @@
         cfg.getBoolean("remote", rc.getName(), "replicatePermissions", true);
     replicateProjectDeletions =
         cfg.getBoolean("remote", rc.getName(), "replicateProjectDeletions", false);
-    remoteNameStyle = Objects.firstNonNull(
+    remoteNameStyle = MoreObjects.firstNonNull(
         cfg.getString("remote", rc.getName(), "remoteNameStyle"), "slash");
     projects = cfg.getStringList("remote", rc.getName(), "projects");
 
@@ -396,36 +396,15 @@
       return true;
     }
 
-    String projectName = project.get();
-    for (final String projectMatch : projects) {
-      if (isRE(projectMatch)) {
-        // projectMatch is a regular expression
-        if (projectName.matches(projectMatch)) {
-          return true;
-        }
-      } else if (isWildcard(projectMatch)) {
-        // projectMatch is a wildcard
-        if (projectName.startsWith(
-            projectMatch.substring(0, projectMatch.length() - 1))) {
-          return true;
-        }
-      } else {
-        // No special case, so we try to match directly
-        if (projectName.equals(projectMatch)) {
-          return true;
-        }
-      }
-    }
-
-    // Nothing matched, so don't push the project
-    return false;
+    return (new ReplicationFilter(Arrays.asList(projects))).matches(project);
   }
 
   boolean isSingleProjectMatch() {
     boolean ret = (projects.length == 1);
     if (ret) {
       String projectMatch = projects[0];
-      if (isRE(projectMatch) || isWildcard(projectMatch)) {
+      if (ReplicationFilter.getPatternType(projectMatch)
+          != ReplicationFilter.PatternType.EXACT_MATCH) {
         // projectMatch is either regular expression, or wild-card.
         //
         // Even though they might refer to a single project now, they need not
@@ -437,14 +416,6 @@
     return ret;
   }
 
-  private static boolean isRE(String str) {
-    return str.startsWith(AccessSection.REGEX_PREFIX);
-  }
-
-  private static boolean isWildcard(String str) {
-    return str.endsWith("*");
-  }
-
   boolean wouldPushRef(String ref) {
     if (!replicatePermissions && RefNames.REFS_CONFIG.equals(ref)) {
       return false;
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 37d47ca..5b737f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
@@ -50,7 +50,8 @@
     if (srvInfo.getState() == ServerInformation.State.STARTUP
         && config.isReplicateAllOnPluginStart()) {
       ReplicationState state = new ReplicationState();
-      pushAllFuture.set(pushAll.create(null, state).schedule(30, TimeUnit.SECONDS));
+      pushAllFuture.set(pushAll.create(
+          null, ReplicationFilter.all(), state).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 720da54..c6ad873 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
@@ -29,22 +29,30 @@
       new ReplicationStateLogger(ReplicationQueue.repLog);
 
   interface Factory {
-    PushAll create(String urlMatch, ReplicationState state);
+    PushAll create(String urlMatch,
+        ReplicationFilter filter,
+        ReplicationState state);
   }
 
   private final WorkQueue workQueue;
   private final ProjectCache projectCache;
   private final ReplicationQueue replication;
   private final String urlMatch;
+  private final ReplicationFilter filter;
   private final ReplicationState state;
 
   @Inject
-  PushAll(WorkQueue wq, ProjectCache projectCache, ReplicationQueue rq,
-      @Assisted @Nullable String urlMatch, @Assisted ReplicationState state) {
+  PushAll(WorkQueue wq,
+      ProjectCache projectCache,
+      ReplicationQueue rq,
+      @Assisted @Nullable String urlMatch,
+      @Assisted ReplicationFilter filter,
+      @Assisted ReplicationState state) {
     this.workQueue = wq;
     this.projectCache = projectCache;
     this.replication = rq;
     this.urlMatch = urlMatch;
+    this.filter = filter;
     this.state = state;
   }
 
@@ -56,7 +64,9 @@
   public void run() {
     try {
       for (Project.NameKey nameKey : projectCache.all()) {
-        replication.scheduleFullSync(nameKey, urlMatch, state);
+        if (filter.matches(nameKey)) {
+          replication.scheduleFullSync(nameKey, urlMatch, state);
+        }
       }
     } catch (Exception e) {
       stateLog.error("Cannot enumerate known projects", e, state);
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 b699e0d..0614959 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -22,7 +22,6 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -221,7 +220,7 @@
   }
 
   ReplicationState[] getStatesAsArray() {
-    Set<ReplicationState> statesSet = new HashSet<ReplicationState>();
+    Set<ReplicationState> statesSet = new HashSet<>();
     statesSet.addAll(stateMap.values());
     return statesSet.toArray(new ReplicationState[statesSet.size()]);
   }
@@ -297,7 +296,7 @@
       // does not exist.  In this case NoRemoteRepositoryException is not
       // raised.
       final String msg = e.getMessage();
-      if (msg.contains("access denied")) {
+      if (msg.contains("access denied") || msg.contains("no such repository")) {
         createRepository();
       } else {
         repLog.error("Cannot replicate " + projectName
@@ -348,21 +347,14 @@
     if (pool.isCreateMissingRepos()) {
       try {
         final Ref head = git.getRef(Constants.HEAD);
-        NewProjectCreatedListener.Event event =
-            new NewProjectCreatedListener.Event() {
-              @Override
-              public String getProjectName() {
-                return projectName.get();
-              }
-
-              @Override
-              public String getHeadName() {
-                return head != null ? head.getName() : null;
-              }
-            };
-        replicationQueue.onNewProjectCreated(event);
-        repLog.warn("Missing repository created; retry replication to " + uri);
-        pool.reschedule(this, Destination.RetryReason.REPOSITORY_MISSING);
+        if (replicationQueue.createProject(projectName, head != null ? head.getName() : null)) {
+          repLog.warn("Missing repository created; retry replication to " + uri);
+          pool.reschedule(this, Destination.RetryReason.REPOSITORY_MISSING);
+        } else {
+          repLog.warn("Missing repository could not be created when replicating " + uri +
+              ". You can only create missing repositories locally, over SSH or when " +
+              "using adminUrl in replication.config. See documentation for more information.");
+        }
       } catch (IOException ioe) {
         stateLog.error("Cannot replicate to " + uri + "; failed to create missing repository",
             ioe, getStatesAsArray());
@@ -552,12 +544,12 @@
 
   private void updateStates(Collection<RemoteRefUpdate> refUpdates)
       throws LockFailureException {
-    Set<String> doneRefs = new HashSet<String>();
+    Set<String> doneRefs = new HashSet<>();
     boolean anyRefFailed = false;
 
     for (RemoteRefUpdate u : refUpdates) {
       RefPushResult pushStatus = RefPushResult.SUCCEEDED;
-      Set<ReplicationState> logStates = new HashSet<ReplicationState>();
+      Set<ReplicationState> logStates = new HashSet<>();
 
       logStates.addAll(stateMap.get(u.getSrcRef()));
       logStates.addAll(stateMap.get(ALL_REFS));
@@ -625,5 +617,5 @@
     public LockFailureException(URIish uri, String message) {
       super(uri, message);
     }
-  };
+  }
 }
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 56c5d39..762f069 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
@@ -14,13 +14,13 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
-import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.RefEvent;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 
@@ -41,10 +41,20 @@
 
   abstract void onAllRefsReplicatedToAllNodes(int totalPushTasksCount);
 
+  /**
+   * Write message to standard out.
+   *
+   * @param message message text.
+   */
   void writeStdOut(final String message) {
     // Default doing nothing
   }
 
+  /**
+   * Write message to standard error.
+   *
+   * @param message message text.
+   */
   void writeStdErr(final String message) {
     // Default doing nothing
   }
@@ -68,7 +78,7 @@
     private AtomicBoolean hasError = new AtomicBoolean();
 
     CommandProcessing(StartCommand sshCommand) {
-      this.sshCommand = new WeakReference<StartCommand>(sshCommand);
+      this.sshCommand = new WeakReference<>(sshCommand);
     }
 
     @Override
@@ -146,11 +156,12 @@
   public static class GitUpdateProcessing extends PushResultProcessing {
     static final Logger log = LoggerFactory.getLogger(GitUpdateProcessing.class);
 
-    private final ChangeHooks hooks;
+    private final EventDispatcher dispatcher;
     private final SchemaFactory<ReviewDb> schema;
 
-    public GitUpdateProcessing(ChangeHooks hooks, SchemaFactory<ReviewDb> schema) {
-      this.hooks = hooks;
+    public GitUpdateProcessing(EventDispatcher dispatcher,
+        SchemaFactory<ReviewDb> schema) {
+      this.dispatcher = dispatcher;
       this.schema = schema;
     }
 
@@ -173,14 +184,14 @@
     void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
     }
 
-    private void postEvent(String project, String ref, ChangeEvent event) {
+    private void postEvent(String project, String ref, RefEvent event) {
       if (PatchSet.isRef(ref)) {
         try {
           ReviewDb db = schema.open();
           try {
             Change change = retrieveChange(ref, db);
             if (change != null) {
-              hooks.postEvent(change, event, db);
+              dispatcher.postEvent(change, event, db);
             }
           } finally {
             db.close();
@@ -190,7 +201,7 @@
         }
       } else {
         Branch.NameKey branch = new Branch.NameKey(Project.NameKey.parse(project), ref);
-        hooks.postEvent(branch, event);
+        dispatcher.postEvent(branch, event);
       }
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
index 98435ac..664e819 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
@@ -14,12 +14,12 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
-import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.events.RefEvent;
 
 import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
 
-public class RefReplicatedEvent extends ChangeEvent {
-  public final String type = "ref-replicated";
+public class RefReplicatedEvent extends RefEvent {
   public final String project;
   public final String ref;
   public final String targetNode;
@@ -27,6 +27,7 @@
 
   public RefReplicatedEvent(String project, String ref, String targetNode,
       RefPushResult status) {
+    super("ref-replicated");
     this.project = project;
     this.ref = ref;
     this.targetNode = targetNode;
@@ -36,4 +37,14 @@
   private String toStatusString(RefPushResult status) {
     return status.name().toLowerCase().replace("_", "-");
   }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(project);
+  }
+
+  @Override
+  public String getRefName() {
+    return ref;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
index 7eaed66..fe92bc8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
@@ -14,17 +14,28 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
-import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.events.RefEvent;
 
-public class RefReplicationDoneEvent extends ChangeEvent {
-  public final String type = "ref-replication-done";
+public class RefReplicationDoneEvent extends RefEvent {
   public final String project;
   public final String ref;
   public final int nodesCount;
 
   public RefReplicationDoneEvent(String project, String ref, int nodesCount) {
+    super("ref-replication-done");
     this.project = project;
     this.ref = ref;
     this.nodesCount = nodesCount;
   }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(project);
+  }
+
+  @Override
+  public String getRefName() {
+    return ref;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
index 5c18f75..241c881 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
@@ -19,7 +19,13 @@
 
 public interface ReplicationConfig {
 
-  List<Destination> getDestinations();
+  enum FilterType {
+    PROJECT_CREATION,
+    PROJECT_DELETION,
+    ALL
+  }
+
+  List<Destination> getDestinations(FilterType filterType);
 
   boolean isReplicateAllOnPluginStart();
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
index 57d07b8..f56185d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
@@ -13,6 +13,8 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.replication;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.server.PluginUser;
@@ -69,14 +71,47 @@
     this.destinations = allDestinations();
   }
 
-  /* (non-Javadoc)
-   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#getDestinations()
+  /*
+   * (non-Javadoc)
+   * @see
+   * com.googlesource.gerrit.plugins.replication.ReplicationConfig#getDestinations
+   * (com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType)
    */
   @Override
-  public List<Destination> getDestinations() {
-    return destinations;
-  }
+  public List<Destination> getDestinations(FilterType filterType) {
+    Predicate<Destination> filter;
+    switch (filterType) {
+      case PROJECT_CREATION :
+        filter = new Predicate<Destination>() {
 
+          @Override
+          public boolean apply(Destination dest) {
+            if (dest == null || !dest.isCreateMissingRepos()) {
+              return false;
+            }
+            return true;
+          }
+        };
+        break;
+      case PROJECT_DELETION :
+        filter = new Predicate<Destination>() {
+
+          @Override
+          public boolean apply(Destination dest) {
+            if (dest == null || !dest.isReplicateProjectDeletions()) {
+              return false;
+            }
+            return true;
+          }
+        };
+        break;
+      case ALL :
+        return destinations;
+      default :
+        return destinations;
+    }
+    return FluentIterable.from(destinations).filter(filter).toList();
+  }
   private List<Destination> allDestinations()
       throws ConfigInvalidException, IOException {
     if (!config.getFile().exists()) {
@@ -184,6 +219,7 @@
     return cfgPath;
   }
 
+  @Override
   public int shutdown() {
     int discarded = 0;
     for (Destination cfg : destinations) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFilter.java
new file mode 100644
index 0000000..6e7be66
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFilter.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2014 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.common.data.AccessSection;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ReplicationFilter {
+  public enum PatternType {
+    REGEX, WILDCARD, EXACT_MATCH;
+  }
+
+  public static ReplicationFilter all() {
+    return new ReplicationFilter(Collections.<String> emptyList());
+  }
+
+  public static PatternType getPatternType(String pattern) {
+    if (pattern.startsWith(AccessSection.REGEX_PREFIX)) {
+      return PatternType.REGEX;
+    } else if (pattern.endsWith("*")) {
+      return PatternType.WILDCARD;
+    } else {
+      return PatternType.EXACT_MATCH;
+    }
+  }
+
+  private final List<String> projectPatterns;
+
+  public ReplicationFilter(List<String> patterns) {
+    projectPatterns = patterns;
+  }
+
+  public boolean matches(NameKey name) {
+    if (projectPatterns.isEmpty()) {
+      return true;
+    } else {
+      String projectName = name.get();
+
+      for (String pattern : projectPatterns) {
+        if (matchesPattern(projectName, pattern)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private boolean matchesPattern(String projectName, String pattern) {
+    boolean match = false;
+    switch (getPatternType(pattern)) {
+      case REGEX:
+        match = projectName.matches(pattern);
+        break;
+      case WILDCARD:
+        match =
+            projectName.startsWith(pattern.substring(0, pattern.length() - 1));
+        break;
+      case EXACT_MATCH:
+        match = projectName.equals(pattern);
+    }
+    return match;
+  }
+}
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 6e2ffdc..b9575b9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
+import static com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult.SUCCEEDED;
 import static com.googlesource.gerrit.plugins.replication.StartReplicationCapability.START_REPLICATION;
 
 import com.google.gerrit.extensions.annotations.Exports;
@@ -24,11 +25,13 @@
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.events.EventTypes;
 import com.google.inject.AbstractModule;
 import com.google.inject.Scopes;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.internal.UniqueAnnotations;
 
+
 class ReplicationModule extends AbstractModule {
   @Override
   protected void configure() {
@@ -59,5 +62,8 @@
     install(new FactoryModuleBuilder().build(RemoteSiteUser.Factory.class));
 
     bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class);
+
+    EventTypes.registerClass(new RefReplicatedEvent(null, null, null, SUCCEEDED));
+    EventTypes.registerClass(new RefReplicationDoneEvent(null, null, 0));
   }
 }
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 8be6b21..789e26a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -29,8 +29,8 @@
 import com.google.inject.Inject;
 
 import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.Constants;
@@ -79,17 +79,16 @@
 
   private final WorkQueue workQueue;
   private final SchemaFactory<ReviewDb> database;
-  private final ChangeHooks changeHooks;
+  private final EventDispatcher dispatcher;
   private final ReplicationConfig config;
   private volatile boolean running;
 
   @Inject
   ReplicationQueue(final WorkQueue wq, final ReplicationConfig rc,
-      final SchemaFactory<ReviewDb> db, final ChangeHooks ch)
-      throws ConfigInvalidException, IOException {
+      final SchemaFactory<ReviewDb> db, final EventDispatcher dis) {
     workQueue = wq;
     database = db;
-    changeHooks = ch;
+    dispatcher = dis;
     config = rc;
   }
 
@@ -117,7 +116,7 @@
       return;
     }
 
-    for (Destination cfg : config.getDestinations()) {
+    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);
@@ -128,14 +127,14 @@
 
   @Override
   public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    ReplicationState state = new ReplicationState(new GitUpdateProcessing(changeHooks, database));
+    ReplicationState state = new ReplicationState(new GitUpdateProcessing(dispatcher, database));
     if (!running) {
       stateLog.warn("Replication plugin did not finish startup before event", state);
       return;
     }
 
     Project.NameKey project = new Project.NameKey(event.getProjectName());
-    for (Destination cfg : config.getDestinations()) {
+    for (Destination cfg : config.getDestinations(FilterType.ALL)) {
       if (cfg.wouldPushProject(project) && cfg.wouldPushRef(event.getRefName())) {
         for (URIish uri : cfg.getURIs(project, null)) {
           cfg.schedule(project, event.getRefName(), uri, state);
@@ -147,28 +146,31 @@
 
   @Override
   public void onNewProjectCreated(NewProjectCreatedListener.Event event) {
-    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), false)) {
+    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()),
+        FilterType.PROJECT_CREATION)) {
       createProject(uri, event.getHeadName());
     }
   }
 
   @Override
   public void onProjectDeleted(ProjectDeletedListener.Event event) {
-    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), true)) {
+    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()),
+        FilterType.PROJECT_DELETION)) {
       deleteProject(uri);
     }
   }
 
   @Override
   public void onHeadUpdated(HeadUpdatedListener.Event event) {
-    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), false)) {
+    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()),
+        FilterType.ALL)) {
       updateHead(uri, event.getNewHeadName());
     }
   }
 
   private Set<URIish> getURIs(Project.NameKey projectName,
-      boolean forProjectDeletion) {
-    if (config.getDestinations().isEmpty()) {
+      FilterType filterType) {
+    if (config.getDestinations(filterType).isEmpty()) {
       return Collections.emptySet();
     }
     if (!running) {
@@ -177,13 +179,11 @@
     }
 
     Set<URIish> uris = Sets.newHashSet();
-    for (Destination config : this.config.getDestinations()) {
+    for (Destination config : this.config.getDestinations(filterType)) {
       if (!config.wouldPushProject(projectName)) {
         continue;
       }
-      if (forProjectDeletion && !config.isReplicateProjectDeletions()) {
-        continue;
-      }
+
       List<URIish> uriList = config.getURIs(projectName, "*");
       String[] adminUrls = config.getAdminUrls();
       boolean adminURLUsed = false;
@@ -230,7 +230,15 @@
     return uris;
   }
 
-  private void createProject(URIish replicateURI, String head) {
+  public boolean createProject(Project.NameKey project, String head) {
+    boolean success = false;
+    for (URIish uri : getURIs(project, FilterType.PROJECT_CREATION)) {
+      success &= createProject(uri, head);
+    }
+    return success;
+  }
+
+  private boolean createProject(URIish replicateURI, String head) {
     if (!replicateURI.isRemote()) {
       createLocally(replicateURI, head);
       repLog.info("Created local repository: " + replicateURI);
@@ -241,7 +249,9 @@
       repLog.warn(String.format("Cannot create new project on remote site %s."
           + " Only local paths and SSH URLs are supported"
           + " for remote repository creation", replicateURI));
+      return false;
     }
+    return true;
   }
 
   private static void createLocally(URIish uri, String head) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
index 68433f1..32d3905 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
@@ -51,6 +51,7 @@
     return cfg;
   }
 
+  @Override
   public SecureCredentialsProvider create(String remoteName) {
     String user = config.getString("remote", remoteName, "username");
     String pass = config.getString("remote", remoteName, "password");
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 44513c0..17a2df9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
@@ -15,8 +15,6 @@
 package com.googlesource.gerrit.plugins.replication;
 
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -49,40 +47,31 @@
       usage = "wait for replication to finish before exiting")
   private boolean wait;
 
-  @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project name")
-  private List<String> projectNames = new ArrayList<String>(2);
+  @Argument(index = 0, multiValued = true, metaVar = "PATTERN", usage = "project name pattern")
+  private List<String> projectPatterns = new ArrayList<>(2);
 
   @Inject
-  private PushAll.Factory pushAllFactory;
-
-  @Inject
-  private ReplicationQueue replication;
-
-  @Inject
-  private ProjectCache projectCache;
+  private PushAll.Factory pushFactory;
 
   @Override
   protected void run() throws Failure {
-    if (all && projectNames.size() > 0) {
+    if (all && projectPatterns.size() > 0) {
       throw new UnloggedFailure(1, "error: cannot combine --all and PROJECT");
     }
 
     ReplicationState state = new ReplicationState(new CommandProcessing(this));
     Future<?> future = null;
+
+    ReplicationFilter projectFilter;
+
     if (all) {
-      future = pushAllFactory.create(urlMatch, state).schedule(0, TimeUnit.SECONDS);
+      projectFilter = ReplicationFilter.all();
     } else {
-      for (String name : projectNames) {
-        Project.NameKey key = new Project.NameKey(name);
-        if (projectCache.get(key) != null) {
-          replication.scheduleFullSync(key, urlMatch, state);
-        } else {
-          writeStdErrSync("error: '" + name + "': not a Gerrit project");
-        }
-      }
-      state.markAllPushTasksScheduled();
+      projectFilter = new ReplicationFilter(projectPatterns);
     }
 
+    future = pushFactory.create(urlMatch, projectFilter, state).schedule(0, TimeUnit.SECONDS);
+
     if (wait) {
       if (future != null) {
         try {
diff --git a/src/main/resources/Documentation/cmd-start.md b/src/main/resources/Documentation/cmd-start.md
index 32da642..49f8c94 100644
--- a/src/main/resources/Documentation/cmd-start.md
+++ b/src/main/resources/Documentation/cmd-start.md
@@ -11,7 +11,7 @@
 ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start
   [--wait]
   [--url <PATTERN>]
-  {--all | <PROJECT> ...}
+  {--all | <PROJECT PATTERN> ...}
 ```
 
 DESCRIPTION
@@ -60,6 +60,19 @@
 pattern in command options, or the authGroup in the replication.config
 has no read access for the replicated projects.
 
+If one or several project patterns are supplied, only those projects
+conforming to both this/these pattern(s) and those defined in
+replication.config for the target host(s) are queued for replication.
+
+The patterns follow the same format as those in replication.config,
+where wildcard or regular expression patterns can be given.
+Regular expression patterns must match a complete project name to be
+considered a match.
+
+A regular expression pattern starts with `^` and a wildcard pattern ends
+with a `*`. If the pattern starts with `^` and ends with `*`, it is
+treated as a regular expression.
+
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
@@ -106,6 +119,18 @@
   $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start tools/gerrit
 ```
 
+Replicate only projects located in the `documentation` subdirectory:
+
+```
+  $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start documentation/*
+```
+
+Replicate projects whose path includes a folder named `vendor` to host slave1:
+
+```
+  $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ start --url slave1 ^(|.*/)vendor(|/.*)
+```
+
 SEE ALSO
 --------
 
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 94b8473..bea2d92 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -238,12 +238,13 @@
 	everything to all remotes.
 
 remote.NAME.createMissingRepositories
-:	If true, a repository is automatically created on the remote site
-	if during the replication of a ref it is found to be missing.
-	E.g. a repository can be missing on the remote site if the remote
-	site was not available at the moment when a new project was created
-	and hence the repository for the new project could not be created on
-	the remote site.
+:	If true, a repository is automatically created on the remote site.
+	If the remote site was not available at the moment when a new
+	project was created, it will be created if during the replication
+	of a ref it is found to be missing.
+	
+	If false, repositories are never created automatically on this
+	remote.
 
 	By default, true, missing repositories are created.
 
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 93ea562..a400dca 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
@@ -24,7 +24,7 @@
 import static org.easymock.EasyMock.reset;
 import static org.easymock.EasyMock.verify;
 
-import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ChangeAccess;
@@ -49,14 +49,15 @@
     KeyUtil.setEncoderImpl(new StandardKeyEncoder());
   }
 
-  private ChangeHooks changeHooksMock;
+  private EventDispatcher dispatcherMock;
   private ChangeAccess changeAccessMock;
   private GitUpdateProcessing gitUpdateProcessing;
 
+  @Override
   protected void setUp() throws Exception {
     super.setUp();
-    changeHooksMock = createMock(ChangeHooks.class);
-    replay(changeHooksMock);
+    dispatcherMock = createMock(EventDispatcher.class);
+    replay(dispatcherMock);
     changeAccessMock = createNiceMock(ChangeAccess.class);
     replay(changeAccessMock);
     ReviewDb reviewDbMock = createNiceMock(ReviewDb.class);
@@ -65,22 +66,22 @@
     SchemaFactory<ReviewDb> schemaMock = createMock(SchemaFactory.class);
     expect(schemaMock.open()).andReturn(reviewDbMock).anyTimes();
     replay(schemaMock);
-    gitUpdateProcessing = new GitUpdateProcessing(changeHooksMock, schemaMock);
+    gitUpdateProcessing = new GitUpdateProcessing(dispatcherMock, schemaMock);
   }
 
   public void testHeadRefReplicated() throws URISyntaxException {
-    reset(changeHooksMock);
+    reset(dispatcherMock);
     RefReplicatedEvent expectedEvent =
         new RefReplicatedEvent("someProject", "refs/heads/master", "someHost",
             RefPushResult.SUCCEEDED);
-    changeHooksMock.postEvent(anyObject(Branch.NameKey.class),
+    dispatcherMock.postEvent(anyObject(Branch.NameKey.class),
         RefReplicatedEventEquals.eqEvent(expectedEvent));
     expectLastCall().once();
-    replay(changeHooksMock);
+    replay(dispatcherMock);
 
     gitUpdateProcessing.onRefReplicatedToOneNode("someProject", "refs/heads/master",
         new URIish("git://someHost/someProject.git"), RefPushResult.SUCCEEDED);
-    verify(changeHooksMock);
+    verify(dispatcherMock);
   }
 
   public void testChangeRefReplicated() throws URISyntaxException, OrmException {
@@ -89,32 +90,32 @@
     expect(changeAccessMock.get(anyObject(Change.Id.class))).andReturn(expectedChange);
     replay(changeAccessMock);
 
-    reset(changeHooksMock);
+    reset(dispatcherMock);
     RefReplicatedEvent expectedEvent =
-        new RefReplicatedEvent("someProject", "refs/changes/1/1/1", "someHost",
+        new RefReplicatedEvent("someProject", "refs/changes/01/1/1", "someHost",
             RefPushResult.FAILED);
-    changeHooksMock.postEvent(eq(expectedChange),
+    dispatcherMock.postEvent(eq(expectedChange),
         RefReplicatedEventEquals.eqEvent(expectedEvent),
         anyObject(ReviewDb.class));
     expectLastCall().once();
-    replay(changeHooksMock);
+    replay(dispatcherMock);
 
     gitUpdateProcessing.onRefReplicatedToOneNode("someProject",
-        "refs/changes/1/1/1", new URIish("git://someHost/someProject.git"),
+        "refs/changes/01/1/1", new URIish("git://someHost/someProject.git"),
         RefPushResult.FAILED);
-    verify(changeHooksMock);
+    verify(dispatcherMock);
   }
 
-  public void testOnAllNodesReplicated() throws URISyntaxException {
-    reset(changeHooksMock);
+  public void testOnAllNodesReplicated() {
+    reset(dispatcherMock);
     RefReplicationDoneEvent expectedDoneEvent =
         new RefReplicationDoneEvent("someProject", "refs/heads/master", 5);
-    changeHooksMock.postEvent(anyObject(Branch.NameKey.class),
+    dispatcherMock.postEvent(anyObject(Branch.NameKey.class),
         RefReplicationDoneEventEquals.eqEvent(expectedDoneEvent));
     expectLastCall().once();
-    replay(changeHooksMock);
+    replay(dispatcherMock);
 
     gitUpdateProcessing.onRefReplicatedToAllNodes("someProject", "refs/heads/master", 5);
-    verify(changeHooksMock);
+    verify(dispatcherMock);
   }
 }