Merge branch 'stable-2.8'

* stable-2.8:
  Never replicate automerge-cache commits
  Fix: failure to create to missing remote repository via git://
  Improve info logging related to repository creation and deletion
  Clarify replication of refs/meta/config when refspec is 'all refs'
  Differentiate error logs for local and remote repository errors

Conflicts:
	src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java

Change-Id: I41eac82e06f35ba45e984e65e970cd967cfeff46
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 2f45466..17904c0 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,9 +1,9 @@
 #Fri Jul 16 23:39:13 PDT 2010
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
-org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.compliance=1.7
 org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.source=1.7
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git a/BUCK b/BUCK
index d8a9358..0018323 100644
--- a/BUCK
+++ b/BUCK
@@ -17,6 +17,11 @@
   srcs = glob(['src/test/java/**/*.java']),
   deps = [
     ':replication__plugin__compile',
+    '//gerrit-common:server',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:easymock',
+    '//lib:gwtorm',
     '//lib:junit',
     '//lib/jgit:jgit',
   ],
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 73f1f39..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>com.googlesource.gerrit.plugins.replication</groupId>
-  <artifactId>replication</artifactId>
-  <name>replication</name>
-  <packaging>jar</packaging>
-  <version>2.8</version>
-
-  <properties>
-    <Gerrit-ApiType>plugin</Gerrit-ApiType>
-    <Gerrit-ApiVersion>${project.version}</Gerrit-ApiVersion>
-  </properties>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-jar-plugin</artifactId>
-        <version>2.4</version>
-        <configuration>
-          <archive>
-            <manifestEntries>
-              <Gerrit-PluginName>replication</Gerrit-PluginName>
-
-              <Gerrit-Module>com.googlesource.gerrit.plugins.replication.ReplicationModule</Gerrit-Module>
-              <Gerrit-SshModule>com.googlesource.gerrit.plugins.replication.SshModule</Gerrit-SshModule>
-
-              <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
-              <Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL>
-
-              <Implementation-Title>Plugin ${project.artifactId}</Implementation-Title>
-              <Implementation-Version>${project.version}</Implementation-Version>
-
-              <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
-              <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
-            </manifestEntries>
-          </archive>
-        </configuration>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <version>2.3.2</version>
-        <configuration>
-          <source>1.6</source>
-          <target>1.6</target>
-          <encoding>UTF-8</encoding>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
-      <version>${Gerrit-ApiVersion}</version>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <version>4.8.1</version>
-      <scope>test</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>commons-io</groupId>
-      <artifactId>commons-io</artifactId>
-      <version>2.4</version>
-    </dependency>
-  </dependencies>
-
-  <repositories>
-    <repository>
-      <id>gerrit-api-repository</id>
-      <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
-    </repository>
-  </repositories>
-</project>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
new file mode 100644
index 0000000..a2b66d1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2013 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.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+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;
+
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+public class AutoReloadConfigDecorator implements ReplicationConfig {
+  private static final Logger log = LoggerFactory
+      .getLogger(AutoReloadConfigDecorator.class);
+  private ReplicationFileBasedConfig currentConfig;
+  private long currentConfigTs;
+
+  private final Injector injector;
+  private final SitePaths site;
+  private final RemoteSiteUser.Factory remoteSiteUserFactory;
+  private final PluginUser pluginUser;
+  private final SchemaFactory<ReviewDb> db;
+  private final GitRepositoryManager gitRepositoryManager;
+  private final GroupBackend groupBackend;
+  private final WorkQueue workQueue;
+
+  @Inject
+  public AutoReloadConfigDecorator(Injector injector, SitePaths site,
+      RemoteSiteUser.Factory ruf, PluginUser pu, SchemaFactory<ReviewDb> db,
+      GitRepositoryManager grm, GroupBackend gb,
+      WorkQueue workQueue) throws ConfigInvalidException,
+      IOException {
+    this.injector = injector;
+    this.site = site;
+    this.remoteSiteUserFactory = ruf;
+    this.pluginUser = pu;
+    this.db = db;
+    this.gitRepositoryManager = grm;
+    this.groupBackend = gb;
+    this.currentConfig = loadConfig();
+    this.currentConfigTs = currentConfig.getCfgPath().lastModified();
+    this.workQueue = workQueue;
+  }
+
+  private ReplicationFileBasedConfig loadConfig()
+      throws ConfigInvalidException, IOException {
+    return new ReplicationFileBasedConfig(injector, site,
+        remoteSiteUserFactory, pluginUser, db, gitRepositoryManager,
+        groupBackend);
+  }
+
+  private synchronized boolean isAutoReload() {
+    return currentConfig.getConfig().getBoolean("gerrit", "autoReload", false);
+  }
+
+  @Override
+  public synchronized List<Destination> getDestinations() {
+    reloadIfNeeded();
+    return currentConfig.getDestinations();
+  }
+
+  private void reloadIfNeeded() {
+    if (isAutoReload()
+        && currentConfig.getCfgPath().lastModified() > currentConfigTs) {
+      try {
+        ReplicationFileBasedConfig newConfig = loadConfig();
+        newConfig.startup(workQueue);
+        int discarded = currentConfig.shutdown();
+
+        this.currentConfig = newConfig;
+        this.currentConfigTs = currentConfig.getCfgPath().lastModified();
+        log.info("Configuration reloaded: "
+            + currentConfig.getDestinations().size() + " destinations, "
+            + discarded + " replication events discarded");
+
+      } catch (Exception e) {
+        log.error(
+            "Cannot reload replication configuration: keeping existing settings",
+            e);
+        return;
+      }
+    }
+  }
+
+  @Override
+  public synchronized boolean isReplicateAllOnPluginStart() {
+    return currentConfig.isReplicateAllOnPluginStart();
+  }
+
+  @Override
+  public synchronized boolean isDefaultForceUpdate() {
+    return currentConfig.isDefaultForceUpdate();
+  }
+
+  @Override
+  public synchronized boolean isEmpty() {
+    return currentConfig.isEmpty();
+  }
+
+  @Override
+  public synchronized int shutdown() {
+    return currentConfig.shutdown();
+  }
+
+  @Override
+  public synchronized void startup(WorkQueue workQueue) {
+    currentConfig.startup(workQueue);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
new file mode 100644
index 0000000..9d6ce69
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2013 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.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class AutoReloadSecureCredentialsFactoryDecorator implements
+    CredentialsFactory {
+  private static final Logger log = LoggerFactory
+      .getLogger(AutoReloadSecureCredentialsFactoryDecorator.class);
+
+  private final AtomicReference<SecureCredentialsFactory> secureCredentialsFactory;
+  private volatile long secureCredentialsFactoryLoadTs;
+  private final SitePaths site;
+  private ReplicationFileBasedConfig config;
+
+  @Inject
+  public AutoReloadSecureCredentialsFactoryDecorator(SitePaths site,
+      ReplicationFileBasedConfig config) throws ConfigInvalidException,
+      IOException {
+    this.site = site;
+    this.config = config;
+    this.secureCredentialsFactory =
+        new AtomicReference<SecureCredentialsFactory>(
+            new SecureCredentialsFactory(site));
+    this.secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
+  }
+
+  private long getSecureConfigLastEditTs() {
+    FileBasedConfig cfg = new FileBasedConfig(site.secure_config, FS.DETECTED);
+    if (cfg.getFile().exists()) {
+      return cfg.getFile().lastModified();
+    } else {
+      return 0L;
+    }
+  }
+
+  @Override
+  public SecureCredentialsProvider create(String remoteName) {
+    if (needsReload()) {
+      try {
+        secureCredentialsFactory.compareAndSet(secureCredentialsFactory.get(),
+            new SecureCredentialsFactory(site));
+        secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
+        log.info("secure.config reloaded as it was updated on the file system");
+      } catch (Exception e) {
+        log.error("Unexpected error while trying to reload "
+            + "secure.config: keeping existing credentials", e);
+      }
+    }
+
+    return secureCredentialsFactory.get().create(remoteName);
+  }
+
+
+  private boolean needsReload() {
+    return config.getConfig().getBoolean("gerrit", "autoReload", false) &&
+        getSecureConfigLastEditTs() != secureCredentialsFactoryLoadTs;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
similarity index 69%
rename from src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
rename to src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
index ff88b87..9ce4a54 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -11,16 +11,10 @@
 // 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;
 
-public enum ReplicationType {
-  /** Replicate all after gerrit startup. */
-  STARTUP,
+interface CredentialsFactory {
 
-  /** Invoke ssh command to replicate. */
-  COMMAND,
+  SecureCredentialsProvider create(String remoteName);
 
-  /** After a git reference is updated, run the replicaton. */
-  GIT_UPDATED;
 }
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 19a9359..0da6cb2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PluginUser;
@@ -235,7 +236,7 @@
           Ref head = git.getRef(Constants.HEAD);
           if (head != null
               && head.isSymbolic()
-              && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
+              && RefNames.REFS_CONFIG.equals(head.getLeaf().getName())) {
             return;
           }
         } catch (IOException err) {
@@ -256,7 +257,7 @@
         pending.put(uri, e);
       }
       e.addRef(ref);
-      state.increasePushTaskCount();
+      state.increasePushTaskCount(project.get(), ref);
       e.addState(ref, state);
     }
   }
@@ -433,7 +434,7 @@
   }
 
   boolean wouldPushRef(String ref) {
-    if (!replicatePermissions && GitRepositoryManager.REF_CONFIG.equals(ref)) {
+    if (!replicatePermissions && RefNames.REFS_CONFIG.equals(ref)) {
       return false;
     }
     for (RefSpec s : remote.getPushRefSpecs()) {
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 c752edc..37d47ca 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
@@ -28,15 +28,18 @@
   private final ServerInformation srvInfo;
   private final PushAll.Factory pushAll;
   private final ReplicationQueue queue;
+  private final ReplicationConfig config;
 
   @Inject
   OnStartStop(
       ServerInformation srvInfo,
       PushAll.Factory pushAll,
-      ReplicationQueue queue) {
+      ReplicationQueue queue,
+      ReplicationConfig config) {
     this.srvInfo = srvInfo;
     this.pushAll = pushAll;
     this.queue = queue;
+    this.config = config;
     this.pushAllFuture = Atomics.newReference();
   }
 
@@ -45,9 +48,8 @@
     queue.start();
 
     if (srvInfo.getState() == ServerInformation.State.STARTUP
-        && queue.replicateAllOnPluginStart) {
-      ReplicationState state =
-          new ReplicationState(ReplicationType.STARTUP);
+        && config.isReplicateAllOnPluginStart()) {
+      ReplicationState state = new ReplicationState();
       pushAllFuture.set(pushAll.create(null, state).schedule(30, TimeUnit.SECONDS));
     }
   }
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 afd7f91..5b6daea 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -23,6 +23,7 @@
 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;
 import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -38,7 +39,6 @@
 import com.google.inject.assistedinject.Assisted;
 
 import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
-
 import com.jcraft.jsch.JSchException;
 
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
@@ -110,7 +110,7 @@
       final SchemaFactory<ReviewDb> s,
       final Destination p,
       final RemoteConfig c,
-      final SecureCredentialsFactory cpFactory,
+      final CredentialsFactory cpFactory,
       final TagCache tc,
       final PerThreadRequestScope.Scoper ts,
       final ChangeCache cc,
@@ -374,6 +374,8 @@
       return new PushResult();
     }
 
+    log.info("Push to " + uri + " references: " + todo);
+
     return tn.push(NullProgressMonitor.INSTANCE, todo);
   }
 
@@ -474,8 +476,8 @@
   }
 
   private boolean canPushRef(String ref, boolean noPerms) {
-    return !(noPerms && GitRepositoryManager.REF_CONFIG.equals(ref)) &&
-        !ref.startsWith(GitRepositoryManager.REFS_CACHE_AUTOMERGE);
+    return !(noPerms && RefNames.REFS_CONFIG.equals(ref)) &&
+        !ref.startsWith(RefNames.REFS_CACHE_AUTOMERGE);
   }
 
   private Map<String, Ref> listRemote(Transport tn)
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 0452b69..bc503ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
@@ -14,17 +14,33 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
+import com.google.gerrit.common.ChangeHooks;
+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.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+
 import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
 
 import org.eclipse.jgit.transport.URIish;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.lang.ref.WeakReference;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 public abstract class PushResultProcessing {
-  abstract void onOneNodeReplicated(String project, String ref, URIish uri, RefPushResult status);
 
-  abstract void onAllNodesReplicated(int totalPushTasksCount);
+  abstract void onRefReplicatedToOneNode(String project, String ref, URIish uri, RefPushResult status);
+
+  abstract void onRefReplicatedToAllNodes(String project, String ref, int nodesCount);
+
+  abstract void onAllRefsReplicatedToAllNodes(int totalPushTasksCount);
 
   void writeStdOut(final String message) {
     // Default doing nothing
@@ -57,11 +73,13 @@
     }
 
     @Override
-    void onOneNodeReplicated(String project, String ref, URIish uri,
+    void onRefReplicatedToOneNode(String project, String ref, URIish uri,
         RefPushResult status) {
       StringBuilder sb = new StringBuilder();
       sb.append("Replicate ");
       sb.append(project);
+      sb.append(" ref ");
+      sb.append(ref);
       sb.append(" to ");
       sb.append(resolveNodeName(uri));
       sb.append(", ");
@@ -84,7 +102,20 @@
     }
 
     @Override
-    void onAllNodesReplicated(int totalPushTasksCount) {
+    void onRefReplicatedToAllNodes(String project, String ref, int nodesCount) {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Replication of ");
+      sb.append(project);
+      sb.append(" ref ");
+      sb.append(ref);
+      sb.append(" completed to ");
+      sb.append(nodesCount);
+      sb.append(" nodes, ");
+      writeStdOut(sb.toString());
+    }
+
+    @Override
+    void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
       if (totalPushTasksCount == 0) {
         return;
       }
@@ -114,26 +145,76 @@
   }
 
   public static class GitUpdateProcessing extends PushResultProcessing {
-    @Override
-    void onOneNodeReplicated(String project, String ref, URIish uri,
-        RefPushResult status) {
-      //TODO: send stream events
+    static final Logger log = LoggerFactory.getLogger(GitUpdateProcessing.class);
+
+    private final ChangeHooks hooks;
+    private final SchemaFactory<ReviewDb> schema;
+
+    public GitUpdateProcessing(ChangeHooks hooks, SchemaFactory<ReviewDb> schema) {
+      this.hooks = hooks;
+      this.schema = schema;
     }
 
     @Override
-    void onAllNodesReplicated(int totalPushTasksCount) {
-      //TODO: send stream events
+    void onRefReplicatedToOneNode(String project, String ref, URIish uri,
+        RefPushResult status) {
+      RefReplicatedEvent event =
+          new RefReplicatedEvent(project, ref, resolveNodeName(uri), status);
+      postEvent(project, ref, event);
+    }
+
+    @Override
+    void onRefReplicatedToAllNodes(String project, String ref, int nodesCount) {
+      RefReplicationDoneEvent event =
+          new RefReplicationDoneEvent(project, ref, nodesCount);
+      postEvent(project, ref, event);
+    }
+
+    @Override
+    void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
+    }
+
+    private void postEvent(String project, String ref, ChangeEvent event) {
+      if (PatchSet.isRef(ref)) {
+        try {
+          ReviewDb db = schema.open();
+          try {
+            hooks.postEvent(retrieveChange(ref, db), event, db);
+          } finally {
+            db.close();
+          }
+        } catch (Exception e) {
+          log.error("Cannot post event", e);
+        }
+      } else {
+        Branch.NameKey branch = new Branch.NameKey(Project.NameKey.parse(project), ref);
+        hooks.postEvent(branch, event);
+      }
+    }
+
+    private Change retrieveChange(String ref, ReviewDb db)
+        throws OrmException, NoSuchChangeException {
+      PatchSet.Id id = PatchSet.Id.fromRef(ref);
+      Change change = db.changes().get(id.getParentKey());
+      if (change == null) {
+        throw new NoSuchChangeException(id.getParentKey());
+      }
+      return change;
     }
   }
 
   public static class NoopProcessing extends PushResultProcessing {
     @Override
-    void onOneNodeReplicated(String project, String ref, URIish uri,
+    void onRefReplicatedToOneNode(String project, String ref, URIish uri,
         RefPushResult status) {
     }
 
     @Override
-    void onAllNodesReplicated(int totalPushTasksCount) {
+    void onRefReplicatedToAllNodes(String project, String ref, int nodesCount) {
+    }
+
+    @Override
+    void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
new file mode 100644
index 0000000..98435ac
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2013 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.server.events.ChangeEvent;
+
+import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
+
+public class RefReplicatedEvent extends ChangeEvent {
+  public final String type = "ref-replicated";
+  public final String project;
+  public final String ref;
+  public final String targetNode;
+  public final String status;
+
+  public RefReplicatedEvent(String project, String ref, String targetNode,
+      RefPushResult status) {
+    this.project = project;
+    this.ref = ref;
+    this.targetNode = targetNode;
+    this.status = toStatusString(status);
+  }
+
+  private String toStatusString(RefPushResult status) {
+    return status.name().toLowerCase().replace("_", "-");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
new file mode 100644
index 0000000..7eaed66
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 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.server.events.ChangeEvent;
+
+public class RefReplicationDoneEvent extends ChangeEvent {
+  public final String type = "ref-replication-done";
+  public final String project;
+  public final String ref;
+  public final int nodesCount;
+
+  public RefReplicationDoneEvent(String project, String ref, int nodesCount) {
+    this.project = project;
+    this.ref = ref;
+    this.nodesCount = nodesCount;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
similarity index 63%
copy from src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
copy to src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
index ff88b87..5c18f75 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -11,16 +11,24 @@
 // 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;
 
-public enum ReplicationType {
-  /** Replicate all after gerrit startup. */
-  STARTUP,
+import com.google.gerrit.server.git.WorkQueue;
 
-  /** Invoke ssh command to replicate. */
-  COMMAND,
+import java.util.List;
 
-  /** After a git reference is updated, run the replicaton. */
-  GIT_UPDATED;
+public interface ReplicationConfig {
+
+  List<Destination> getDestinations();
+
+  boolean isReplicateAllOnPluginStart();
+
+  boolean isDefaultForceUpdate();
+
+  boolean isEmpty();
+
+  int shutdown();
+
+  void startup(WorkQueue workQueue);
+
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
new file mode 100644
index 0000000..dd8f4f5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
@@ -0,0 +1,209 @@
+// Copyright (C) 2013 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.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@Singleton
+public class ReplicationFileBasedConfig implements ReplicationConfig {
+  static final Logger log = LoggerFactory.getLogger(ReplicationFileBasedConfig.class);
+  private List<Destination> destinations;
+  private File cfgPath;
+  private boolean replicateAllOnPluginStart;
+  private boolean defaultForceUpdate;
+  private Injector injector;
+  private final SchemaFactory<ReviewDb> database;
+  private final RemoteSiteUser.Factory replicationUserFactory;
+  private final PluginUser pluginUser;
+  private final GitRepositoryManager gitRepositoryManager;
+  private final GroupBackend groupBackend;
+  private final FileBasedConfig config;
+
+  @Inject
+  public ReplicationFileBasedConfig(final Injector injector, final SitePaths site,
+      final RemoteSiteUser.Factory ruf, final PluginUser pu,
+      final SchemaFactory<ReviewDb> db, final GitRepositoryManager grm,
+      final GroupBackend gb) throws ConfigInvalidException, IOException {
+    this.cfgPath = new File(site.etc_dir, "replication.config");
+    this.injector = injector;
+    this.replicationUserFactory = ruf;
+    this.pluginUser = pu;
+    this.database = db;
+    this.gitRepositoryManager = grm;
+    this.groupBackend = gb;
+    this.config = new FileBasedConfig(cfgPath, FS.DETECTED);
+    this.destinations = allDestinations();
+  }
+
+  /* (non-Javadoc)
+   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#getDestinations()
+   */
+  @Override
+  public List<Destination> getDestinations() {
+    return destinations;
+  }
+
+  private List<Destination> allDestinations()
+      throws ConfigInvalidException, IOException {
+    if (!config.getFile().exists()) {
+      log.warn("Config file " + config.getFile() + "does not exist; not replicating");
+      return Collections.emptyList();
+    }
+    if (config.getFile().length() == 0) {
+      log.info("Config file " + config.getFile() + " is empty; not replicating");
+      return Collections.emptyList();
+    }
+
+    try {
+      config.load();
+    } catch (ConfigInvalidException e) {
+      throw new ConfigInvalidException(String.format(
+          "Config file %s is invalid: %s", config.getFile(), e.getMessage()), e);
+    } catch (IOException e) {
+      throw new IOException(String.format("Cannot read %s: %s", config.getFile(),
+          e.getMessage()), e);
+    }
+
+    replicateAllOnPluginStart =
+        config.getBoolean("gerrit", "replicateOnStartup", true);
+
+    defaultForceUpdate =
+        config.getBoolean("gerrit", "defaultForceUpdate", false);
+
+    ImmutableList.Builder<Destination> dest = ImmutableList.builder();
+    for (RemoteConfig c : allRemotes(config)) {
+      if (c.getURIs().isEmpty()) {
+        continue;
+      }
+
+      // If destination for push is not set assume equal to source.
+      for (RefSpec ref : c.getPushRefSpecs()) {
+        if (ref.getDestination() == null) {
+          ref.setDestination(ref.getSource());
+        }
+      }
+
+      if (c.getPushRefSpecs().isEmpty()) {
+        c.addPushRefSpec(new RefSpec().setSourceDestination("refs/*", "refs/*")
+            .setForceUpdate(defaultForceUpdate));
+      }
+
+      Destination destination =
+          new Destination(injector, c, config, database, replicationUserFactory,
+              pluginUser, gitRepositoryManager, groupBackend);
+
+      if (!destination.isSingleProjectMatch()) {
+        for (URIish u : c.getURIs()) {
+          if (u.getPath() == null || !u.getPath().contains("${name}")) {
+            throw new ConfigInvalidException(String.format(
+                "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
+                c.getName(), u, config.getFile()));
+          }
+        }
+      }
+
+      dest.add(destination);
+    }
+    return dest.build();
+  }
+
+  /* (non-Javadoc)
+   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isReplicateAllOnPluginStart()
+   */
+  @Override
+  public boolean isReplicateAllOnPluginStart() {
+    return replicateAllOnPluginStart;
+  }
+
+  /* (non-Javadoc)
+   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isDefaultForceUpdate()
+   */
+  @Override
+  public boolean isDefaultForceUpdate() {
+    return defaultForceUpdate;
+  }
+
+  private static List<RemoteConfig> allRemotes(FileBasedConfig cfg)
+      throws ConfigInvalidException {
+    Set<String> names = cfg.getSubsections("remote");
+    List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
+    for (String name : names) {
+      try {
+        result.add(new RemoteConfig(cfg, name));
+      } catch (URISyntaxException e) {
+        throw new ConfigInvalidException(String.format(
+            "remote %s has invalid URL in %s", name, cfg.getFile()));
+      }
+    }
+    return result;
+  }
+
+  /* (non-Javadoc)
+   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isEmpty()
+   */
+  @Override
+  public boolean isEmpty() {
+    return destinations.isEmpty();
+  }
+
+  File getCfgPath() {
+    return cfgPath;
+  }
+
+  public int shutdown() {
+    int discarded = 0;
+    for (Destination cfg : destinations) {
+      discarded += cfg.shutdown();
+    }
+    return discarded;
+  }
+
+  FileBasedConfig getConfig() {
+    return config;
+  }
+
+  @Override
+  public void startup(WorkQueue workQueue) {
+    for (Destination cfg : destinations) {
+      cfg.start(workQueue);
+    }
+  }
+}
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 77823f3..8aa3248 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -35,23 +36,26 @@
 
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
         .to(ReplicationQueue.class);
-
     DynamicSet.bind(binder(), NewProjectCreatedListener.class)
         .to(ReplicationQueue.class);
-
     DynamicSet.bind(binder(), ProjectDeletedListener.class)
         .to(ReplicationQueue.class);
+    DynamicSet.bind(binder(), HeadUpdatedListener.class)
+        .to(ReplicationQueue.class);
 
     bind(OnStartStop.class).in(Scopes.SINGLETON);
     bind(LifecycleListener.class)
       .annotatedWith(UniqueAnnotations.create())
       .to(OnStartStop.class);
-    bind(SecureCredentialsFactory.class).in(Scopes.SINGLETON);
+    bind(CredentialsFactory.class).to(
+        AutoReloadSecureCredentialsFactoryDecorator.class).in(Scopes.SINGLETON);
     bind(CapabilityDefinition.class)
       .annotatedWith(Exports.named(START_REPLICATION))
       .to(StartReplicationCapability.class);
 
     install(new FactoryModuleBuilder().build(PushAll.Factory.class));
     install(new FactoryModuleBuilder().build(RemoteSiteUser.Factory.class));
+
+    bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class);
   }
 }
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 cabb48b..e28060b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -15,23 +15,20 @@
 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.common.ChangeHooks;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
 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;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
+
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.TransportException;
@@ -39,9 +36,6 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.RemoteSession;
 import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.transport.URIish;
@@ -64,7 +58,8 @@
     LifecycleListener,
     GitReferenceUpdatedListener,
     NewProjectCreatedListener,
-    ProjectDeletedListener {
+    ProjectDeletedListener,
+    HeadUpdatedListener {
   static final Logger log = LoggerFactory.getLogger(ReplicationQueue.class);
   private static final WrappedLogger wrappedLog = new WrappedLogger(log);
 
@@ -80,48 +75,32 @@
     return null;
   }
 
-  private final Injector injector;
   private final WorkQueue workQueue;
-  private final List<Destination> configs;
   private final SchemaFactory<ReviewDb> database;
-  private final RemoteSiteUser.Factory replicationUserFactory;
-  private final PluginUser pluginUser;
-  private final GitRepositoryManager gitRepositoryManager;
-  private final GroupBackend groupBackend;
+  private final ChangeHooks changeHooks;
+  private final ReplicationConfig config;
   private volatile boolean running;
-  boolean replicateAllOnPluginStart;
 
   @Inject
-  ReplicationQueue(final Injector i, final WorkQueue wq, final SitePaths site,
-      final RemoteSiteUser.Factory ruf, final PluginUser pu,
-      final SchemaFactory<ReviewDb> db,
-      final GitRepositoryManager grm, final GroupBackend gb)
+  ReplicationQueue(final WorkQueue wq, final ReplicationConfig rc,
+      final SchemaFactory<ReviewDb> db, final ChangeHooks ch)
       throws ConfigInvalidException, IOException {
-    injector = i;
     workQueue = wq;
     database = db;
-    replicationUserFactory = ruf;
-    pluginUser = pu;
-    gitRepositoryManager = grm;
-    groupBackend = gb;
-    configs = allDestinations(new File(site.etc_dir, "replication.config"));
+    changeHooks = ch;
+    config = rc;
   }
 
   @Override
   public void start() {
-    for (Destination cfg : configs) {
-      cfg.start(workQueue);
-    }
+    config.startup(workQueue);
     running = true;
   }
 
   @Override
   public void stop() {
     running = false;
-    int discarded = 0;
-    for (Destination cfg : configs) {
-      discarded += cfg.shutdown();
-    }
+    int discarded = config.shutdown();
     if (discarded > 0) {
       log.warn(String.format(
           "Cancelled %d replication events during shutdown",
@@ -136,7 +115,7 @@
       return;
     }
 
-    for (Destination cfg : configs) {
+    for (Destination cfg : config.getDestinations()) {
       if (cfg.wouldPushProject(project)) {
         for (URIish uri : cfg.getURIs(project, urlMatch)) {
           cfg.schedule(project, PushOne.ALL_REFS, uri, state);
@@ -147,16 +126,14 @@
 
   @Override
   public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    ReplicationState state =
-        new ReplicationState(ReplicationType.GIT_UPDATED);
-
+    ReplicationState state = new ReplicationState(new GitUpdateProcessing(changeHooks, database));
     if (!running) {
       wrappedLog.warn("Replication plugin did not finish startup before event", state);
       return;
     }
 
     Project.NameKey project = new Project.NameKey(event.getProjectName());
-    for (Destination cfg : configs) {
+    for (Destination cfg : config.getDestinations()) {
       if (cfg.wouldPushProject(project) && cfg.wouldPushRef(event.getRefName())) {
         for (URIish uri : cfg.getURIs(project, null)) {
           cfg.schedule(project, event.getRefName(), uri, state);
@@ -166,85 +143,6 @@
     state.markAllPushTasksScheduled();
   }
 
-  private List<Destination> allDestinations(File cfgPath)
-      throws ConfigInvalidException, IOException {
-    FileBasedConfig cfg = new FileBasedConfig(cfgPath, FS.DETECTED);
-    if (!cfg.getFile().exists()) {
-      log.warn("No " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
-    if (cfg.getFile().length() == 0) {
-      log.info("Empty " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
-
-    try {
-      cfg.load();
-    } catch (ConfigInvalidException e) {
-      throw new ConfigInvalidException(String.format(
-          "Config file %s is invalid: %s",cfg.getFile(), e.getMessage()), e);
-    } catch (IOException e) {
-      throw new IOException(String.format(
-          "Cannot read %s: %s", cfg.getFile(),  e.getMessage()), e);
-    }
-
-    replicateAllOnPluginStart = cfg.getBoolean(
-        "gerrit", "replicateOnStartup",
-        true);
-
-    ImmutableList.Builder<Destination> dest = ImmutableList.builder();
-    for (RemoteConfig c : allRemotes(cfg)) {
-      if (c.getURIs().isEmpty()) {
-        continue;
-      }
-
-      // If destination for push is not set assume equal to source.
-      for (RefSpec ref : c.getPushRefSpecs()) {
-        if (ref.getDestination() == null) {
-          ref.setDestination(ref.getSource());
-        }
-      }
-
-      if (c.getPushRefSpecs().isEmpty()) {
-        c.addPushRefSpec(new RefSpec()
-          .setSourceDestination("refs/*", "refs/*")
-          .setForceUpdate(true));
-      }
-
-      Destination destination = new Destination(injector, c, cfg, database,
-          replicationUserFactory, pluginUser, gitRepositoryManager,
-          groupBackend);
-
-      if (!destination.isSingleProjectMatch()) {
-        for (URIish u : c.getURIs()) {
-          if (u.getPath() == null || !u.getPath().contains("${name}")) {
-            throw new ConfigInvalidException(String.format(
-                "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
-                c.getName(), u, cfg.getFile()));
-          }
-        }
-      }
-
-      dest.add(destination);
-    }
-    return dest.build();
-  }
-
-  private static List<RemoteConfig> allRemotes(FileBasedConfig cfg)
-      throws ConfigInvalidException {
-    Set<String> names = cfg.getSubsections("remote");
-    List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
-    for (String name : names) {
-      try {
-        result.add(new RemoteConfig(cfg, name));
-      } catch (URISyntaxException e) {
-        throw new ConfigInvalidException(String.format(
-            "remote %s has invalid URL in %s", name, cfg.getFile()));
-      }
-    }
-    return result;
-  }
-
   @Override
   public void onNewProjectCreated(NewProjectCreatedListener.Event event) {
     for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), false)) {
@@ -259,9 +157,16 @@
     }
   }
 
+  @Override
+  public void onHeadUpdated(HeadUpdatedListener.Event event) {
+    for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), false)) {
+      updateHead(uri, event.getNewHeadName());
+    }
+  }
+
   private Set<URIish> getURIs(Project.NameKey projectName,
       boolean forProjectDeletion) {
-    if (configs.isEmpty()) {
+    if (config.getDestinations().isEmpty()) {
       return Collections.emptySet();
     }
     if (!running) {
@@ -270,7 +175,7 @@
     }
 
     Set<URIish> uris = Sets.newHashSet();
-    for (Destination config : configs) {
+    for (Destination config : this.config.getDestinations()) {
       if (!config.wouldPushProject(projectName)) {
         continue;
       }
@@ -365,7 +270,7 @@
     }
     OutputStream errStream = newErrorBufferStream();
     try {
-      executeRemotSsh(uri, cmd, errStream);
+      executeRemoteSsh(uri, cmd, errStream);
     } catch (IOException e) {
       log.error(String.format(
              "Error creating remote repository at %s:\n"
@@ -423,7 +328,7 @@
     String cmd = "rm -rf " + quotedPath;
     OutputStream errStream = newErrorBufferStream();
     try {
-      executeRemotSsh(uri, cmd, errStream);
+      executeRemoteSsh(uri, cmd, errStream);
     } catch (IOException e) {
       log.error(String.format(
              "Error deleting remote repository at %s:\n"
@@ -434,7 +339,52 @@
     }
   }
 
-  private static void executeRemotSsh(URIish uri, String cmd,
+  private void updateHead(URIish replicateURI, String newHead) {
+    if (!replicateURI.isRemote()) {
+      updateHeadLocally(replicateURI, newHead);
+    } else if (isSSH(replicateURI)) {
+      updateHeadRemoteSsh(replicateURI, newHead);
+    } else {
+      log.warn(String.format("Cannot update HEAD of project on remote site %s."
+          + " Only local paths and SSH URLs are supported"
+          + " for remote HEAD update.", replicateURI));
+    }
+  }
+
+  private static void updateHeadRemoteSsh(URIish uri, String newHead) {
+    String quotedPath = QuotedString.BOURNE.quote(uri.getPath());
+    String cmd = "cd " + quotedPath
+            + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(newHead);
+    OutputStream errStream = newErrorBufferStream();
+    try {
+      executeRemoteSsh(uri, cmd, errStream);
+    } catch (IOException e) {
+      log.error(String.format(
+             "Error updating HEAD of remote repository at %s to %s:\n"
+          + "  Exception: %s\n"
+          + "  Command: %s\n"
+          + "  Output: %s",
+          uri, newHead, e, cmd, errStream), e);
+    }
+  }
+
+  private static void updateHeadLocally(URIish uri, String newHead) {
+    try {
+      Repository repo = new FileRepository(uri.getPath());
+      try {
+        if (newHead != null) {
+          RefUpdate u = repo.updateRef(Constants.HEAD);
+          u.link(newHead);
+        }
+      } finally {
+        repo.close();
+      }
+    } catch (IOException e) {
+      log.error(String.format("Failed to update HEAD of repository %s to %s", uri.getPath(), newHead), e);
+    }
+  }
+
+  private static void executeRemoteSsh(URIish uri, String cmd,
       OutputStream errStream) throws IOException {
     RemoteSession ssh = connect(uri);
     Process proc = ssh.exec(cmd, 0);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
index 35f086f..9632d5f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
@@ -14,16 +14,17 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.NoopProcessing;
+
 import org.eclipse.jgit.transport.URIish;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
-import com.googlesource.gerrit.plugins.replication.PushResultProcessing.CommandProcessing;
-import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
-import com.googlesource.gerrit.plugins.replication.PushResultProcessing.NoopProcessing;
-
 public class ReplicationState {
   private boolean allScheduled;
   private final PushResultProcessing pushResultProcessing;
@@ -31,31 +32,38 @@
   private final Lock countingLock = new ReentrantLock();
   private final CountDownLatch allPushTasksFinished = new CountDownLatch(1);
 
+  private static class RefReplicationStatus {
+    private final String project;
+    private final String ref;
+    private int nodesToReplicateCount;
+    private int replicatedNodesCount;
+
+    public RefReplicationStatus(String project, String ref) {
+      this.project = project;
+      this.ref = ref;
+    }
+
+    public boolean allDone() {
+      return replicatedNodesCount == nodesToReplicateCount;
+    }
+  }
+  private final Table<String, String, RefReplicationStatus> statusByProjectRef;
   private int totalPushTasksCount;
   private int finishedPushTasksCount;
 
-  public ReplicationState(ReplicationType type) {
-    this(type, null);
+  public ReplicationState() {
+    this(new NoopProcessing());
   }
 
-  public ReplicationState(ReplicationType type, StartCommand sshCommand) {
-    switch(type) {
-      case COMMAND:
-        pushResultProcessing = new CommandProcessing(sshCommand);
-        break;
-      case GIT_UPDATED:
-        pushResultProcessing = new GitUpdateProcessing();
-        break;
-      case STARTUP:
-      default:
-        pushResultProcessing = new NoopProcessing();
-        break;
-    }
+  public ReplicationState(PushResultProcessing processing) {
+    pushResultProcessing = processing;
+    statusByProjectRef = HashBasedTable.create();
   }
 
-  public void increasePushTaskCount() {
+  public void increasePushTaskCount(String project, String ref) {
     countingLock.lock();
     try {
+      getRefStatus(project, ref).nodesToReplicateCount++;
       totalPushTasksCount++;
     } finally {
       countingLock.unlock();
@@ -68,22 +76,33 @@
 
   public void notifyRefReplicated(String project, String ref, URIish uri,
       RefPushResult status) {
-    pushResultProcessing.onOneNodeReplicated(project, ref, uri, status);
+    pushResultProcessing.onRefReplicatedToOneNode(project, ref, uri, status);
 
+    RefReplicationStatus completedRefStatus = null;
+    boolean allPushTaksCompleted = false;
     countingLock.lock();
     try {
+      RefReplicationStatus refStatus = getRefStatus(project, ref);
+      refStatus.replicatedNodesCount++;
       finishedPushTasksCount++;
-      if (!allScheduled) {
-        return;
-      }
-      if (finishedPushTasksCount < totalPushTasksCount) {
-        return;
+
+      if (allScheduled) {
+        if (refStatus.allDone()) {
+          completedRefStatus = statusByProjectRef.remove(project, ref);
+        }
+        allPushTaksCompleted = finishedPushTasksCount == totalPushTasksCount;
       }
     } finally {
       countingLock.unlock();
     }
 
-    doAllPushTasksCompleted();
+    if (completedRefStatus != null) {
+      doRefPushTasksCompleted(completedRefStatus);
+    }
+
+    if (allPushTaksCompleted) {
+      doAllPushTasksCompleted();
+    }
   }
 
   public void markAllPushTasksScheduled() {
@@ -101,10 +120,35 @@
   }
 
   private void doAllPushTasksCompleted() {
-    pushResultProcessing.onAllNodesReplicated(totalPushTasksCount);
+    fireRemainingOnRefReplicatedToAllNodes();
+    pushResultProcessing.onAllRefsReplicatedToAllNodes(totalPushTasksCount);
     allPushTasksFinished.countDown();
   }
 
+  /**
+   * Some could be remaining if replication of a ref is completed before all
+   * tasks are scheduled.
+   */
+  private void fireRemainingOnRefReplicatedToAllNodes() {
+    for (RefReplicationStatus refStatus : statusByProjectRef.values()) {
+      doRefPushTasksCompleted(refStatus);
+    }
+  }
+
+  private void doRefPushTasksCompleted(RefReplicationStatus refStatus) {
+    pushResultProcessing.onRefReplicatedToAllNodes(refStatus.project,
+        refStatus.ref, refStatus.nodesToReplicateCount);
+  }
+
+  private RefReplicationStatus getRefStatus(String project, String ref) {
+    if (!statusByProjectRef.contains(project, ref)) {
+      RefReplicationStatus refStatus = new RefReplicationStatus(project, ref);
+      statusByProjectRef.put(project, ref, refStatus);
+      return refStatus;
+    }
+    return statusByProjectRef.get(project, ref);
+  }
+
   public void waitForReplication() throws InterruptedException {
     allPushTasksFinished.await();
   }
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 7bb9831..68433f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
@@ -25,7 +25,7 @@
 import java.io.IOException;
 
 /** Looks up a remote's password in secure.config. */
-class SecureCredentialsFactory {
+class SecureCredentialsFactory implements CredentialsFactory {
   private final Config config;
 
   @Inject
@@ -51,7 +51,7 @@
     return cfg;
   }
 
-  SecureCredentialsProvider create(String remoteName) {
+  public SecureCredentialsProvider create(String remoteName) {
     String user = config.getString("remote", remoteName, "username");
     String pass = config.getString("remote", remoteName, "password");
     return new SecureCredentialsProvider(user, pass);
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 b80c462..9db0ac5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
@@ -21,6 +21,8 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.CommandProcessing;
+
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
@@ -65,8 +67,7 @@
       throw new UnloggedFailure(1, "error: cannot combine --all and PROJECT");
     }
 
-    ReplicationState state =
-        new ReplicationState(ReplicationType.COMMAND, this);
+    ReplicationState state = new ReplicationState(new CommandProcessing(this));
     Future<?> future = null;
     if (all) {
       future = pushAllFactory.create(urlMatch, state).schedule(0, TimeUnit.SECONDS);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 0737a94..e8f9473 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -67,6 +67,16 @@
 :	If true, replicates to all remotes on startup to ensure they
 	are in-sync with this server.  By default, true.
 
+gerrit.autoReload
+:	If true, automatically reloads replication destinations and settings
+	after `replication.config` file is updated, without the need to restart
+	the replication plugin. When the reload takes place, pending replication
+	events based on old settings are discarded. By default, false.
+
+gerrit.defaultForceUpdate
+:	If true, the default push refspec will be set to use forced
+	update to the remote when no refspec is given.  By default, false.
+
 remote.NAME.url
 :	Address of the remote server to push to.  Multiple URLs may be
 	specified within a single remote block, listing different
@@ -122,13 +132,22 @@
 	the active branches, but not the change refs under
 	`refs/changes/`, or the tags under `refs/tags/`.
 
+	Note that prefixing a source refspec with `+` causes the replication
+	to be done with a `git push --force` command.
+	Be aware that when you are pushing to remote repositories that may
+	have read/write access (e.g. GitHub) you may want to omit the `+`
+	to prevent the risk of overwriting branches that have been modified
+	on the remote.
+
 	Multiple push keys can be supplied, to specify multiple
 	patterns to match against.  In the [example above][2], remote
 	"pubmirror" uses two push keys to match both `refs/heads/*`
 	and `refs/tags/*`, but excludes all others, including
 	`refs/changes/*`.
 
-	Defaults to `+refs/*:refs/*` (all refs) if not specified.
+	Defaults to `refs/*:refs/*` (push all refs) if not specified,
+	or `+refs/*:refs/*` (force push all refs) if not specified and
+	`gerrit.defaultForceUpdate` is true.
 
 	Note that the `refs/meta/config` branch is only replicated
 	when `replicatePermissions` is true, even if the push refspec
@@ -216,7 +235,7 @@
 	By default, false, do *not* replicate project deletions.
 
 remote.NAME.mirror
-:	If true, replication will remove remote branches that absent
+:	If true, replication will remove remote branches that are absent
 	locally or invisible to the replication (for example read
 	access denied via `authGroup` option).
 
@@ -228,18 +247,20 @@
 	placeholder.
 
 	Github and Gitorious do not permit slashes "/" in repository
-	names and changes this to dashes "-" at repository creation
-	time. If set to "dash", this changes slashes to dashes in the
-	repository name. If set to "underscore", this changes slashes
-	to underscores in the repository name.
+	names and will change them to dashes "-" at repository creation
+	time.
+
+	If this setting is set to "dash", slashes will be replaced with
+	dashes in the remote repository name. If set to "underscore",
+	slashes will be replaced with underscores in the repository name.
 
 	Option "basenameOnly" makes `${name}` to be only the basename
 	(the part after the last slash) of the repository path on the
 	Gerrit server, e.g. `${name}` of `foo/bar/my-repo.git` would
 	be `my-repo`.
 
-	By default, "slash" remote name will contain slashes as they
-	do in Gerrit.
+	By default, "slash", i.e. remote names will contain slashes as
+	they do in Gerrit.
 
 <a name="remote.NAME.projects">remote.NAME.projects</a>
 :	Specifies which repositories should be replicated to the
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
new file mode 100644
index 0000000..93ea562
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2013 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 static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
+import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jgit.transport.URIish;
+
+import java.net.URISyntaxException;
+
+@SuppressWarnings("unchecked")
+public class GitUpdateProcessingTest extends TestCase {
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  private ChangeHooks changeHooksMock;
+  private ChangeAccess changeAccessMock;
+  private GitUpdateProcessing gitUpdateProcessing;
+
+  protected void setUp() throws Exception {
+    super.setUp();
+    changeHooksMock = createMock(ChangeHooks.class);
+    replay(changeHooksMock);
+    changeAccessMock = createNiceMock(ChangeAccess.class);
+    replay(changeAccessMock);
+    ReviewDb reviewDbMock = createNiceMock(ReviewDb.class);
+    expect(reviewDbMock.changes()).andReturn(changeAccessMock).anyTimes();
+    replay(reviewDbMock);
+    SchemaFactory<ReviewDb> schemaMock = createMock(SchemaFactory.class);
+    expect(schemaMock.open()).andReturn(reviewDbMock).anyTimes();
+    replay(schemaMock);
+    gitUpdateProcessing = new GitUpdateProcessing(changeHooksMock, schemaMock);
+  }
+
+  public void testHeadRefReplicated() throws URISyntaxException {
+    reset(changeHooksMock);
+    RefReplicatedEvent expectedEvent =
+        new RefReplicatedEvent("someProject", "refs/heads/master", "someHost",
+            RefPushResult.SUCCEEDED);
+    changeHooksMock.postEvent(anyObject(Branch.NameKey.class),
+        RefReplicatedEventEquals.eqEvent(expectedEvent));
+    expectLastCall().once();
+    replay(changeHooksMock);
+
+    gitUpdateProcessing.onRefReplicatedToOneNode("someProject", "refs/heads/master",
+        new URIish("git://someHost/someProject.git"), RefPushResult.SUCCEEDED);
+    verify(changeHooksMock);
+  }
+
+  public void testChangeRefReplicated() throws URISyntaxException, OrmException {
+    Change expectedChange = new Change(null, null, null, null, null);
+    reset(changeAccessMock);
+    expect(changeAccessMock.get(anyObject(Change.Id.class))).andReturn(expectedChange);
+    replay(changeAccessMock);
+
+    reset(changeHooksMock);
+    RefReplicatedEvent expectedEvent =
+        new RefReplicatedEvent("someProject", "refs/changes/1/1/1", "someHost",
+            RefPushResult.FAILED);
+    changeHooksMock.postEvent(eq(expectedChange),
+        RefReplicatedEventEquals.eqEvent(expectedEvent),
+        anyObject(ReviewDb.class));
+    expectLastCall().once();
+    replay(changeHooksMock);
+
+    gitUpdateProcessing.onRefReplicatedToOneNode("someProject",
+        "refs/changes/1/1/1", new URIish("git://someHost/someProject.git"),
+        RefPushResult.FAILED);
+    verify(changeHooksMock);
+  }
+
+  public void testOnAllNodesReplicated() throws URISyntaxException {
+    reset(changeHooksMock);
+    RefReplicationDoneEvent expectedDoneEvent =
+        new RefReplicationDoneEvent("someProject", "refs/heads/master", 5);
+    changeHooksMock.postEvent(anyObject(Branch.NameKey.class),
+        RefReplicationDoneEventEquals.eqEvent(expectedDoneEvent));
+    expectLastCall().once();
+    replay(changeHooksMock);
+
+    gitUpdateProcessing.onRefReplicatedToAllNodes("someProject", "refs/heads/master", 5);
+    verify(changeHooksMock);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java
new file mode 100644
index 0000000..c68ba73
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2013 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 org.easymock.EasyMock;
+import org.easymock.IArgumentMatcher;
+
+public class RefReplicatedEventEquals implements IArgumentMatcher {
+
+  private RefReplicatedEvent expected;
+
+  public RefReplicatedEventEquals(RefReplicatedEvent expected) {
+    this.expected = expected;
+  }
+
+  public static final RefReplicatedEvent eqEvent(RefReplicatedEvent refReplicatedEvent) {
+    EasyMock.reportMatcher(new RefReplicatedEventEquals(refReplicatedEvent));
+    return null;
+  }
+
+  @Override
+  public boolean matches(Object actual) {
+    if (!(actual instanceof RefReplicatedEvent)) {
+      return false;
+    }
+    RefReplicatedEvent actualRefReplicatedEvent = (RefReplicatedEvent)actual;
+    if (!equals(expected.project, actualRefReplicatedEvent.project)) {
+      return false;
+    }
+    if (!equals(expected.ref, actualRefReplicatedEvent.ref)) {
+      return false;
+    }
+    if (!equals(expected.targetNode, actualRefReplicatedEvent.targetNode)) {
+      return false;
+    }
+    if (!equals(expected.status, actualRefReplicatedEvent.status)) {
+      return false;
+    }
+    return true;
+  }
+
+  private static boolean equals(Object object1, Object object2) {
+    if (object1 == object2) {
+      return true;
+    }
+    if (object1 != null && !object1.equals(object2)) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public void appendTo(StringBuffer buffer) {
+    buffer.append("eqEvent(");
+    buffer.append(expected.getClass().getName());
+    buffer.append(" with project \"");
+    buffer.append(expected.project);
+    buffer.append(" and ref \"");
+    buffer.append(expected.ref);
+    buffer.append(" and targetNode \"");
+    buffer.append(expected.targetNode);
+    buffer.append(" and status \"");
+    buffer.append(expected.status);
+    buffer.append("\")");
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java
new file mode 100644
index 0000000..42a25de
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2013 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 org.easymock.EasyMock;
+import org.easymock.IArgumentMatcher;
+
+public class RefReplicationDoneEventEquals implements IArgumentMatcher {
+
+  private RefReplicationDoneEvent expected;
+
+  public RefReplicationDoneEventEquals(RefReplicationDoneEvent expected) {
+    this.expected = expected;
+  }
+
+  public static final RefReplicationDoneEvent eqEvent(RefReplicationDoneEvent refReplicatedEvent) {
+    EasyMock.reportMatcher(new RefReplicationDoneEventEquals(refReplicatedEvent));
+    return null;
+  }
+
+  @Override
+  public boolean matches(Object actual) {
+    if (!(actual instanceof RefReplicationDoneEvent)) {
+      return false;
+    }
+    RefReplicationDoneEvent actualRefReplicatedDoneEvent = (RefReplicationDoneEvent)actual;
+    if (!equals(expected.project, actualRefReplicatedDoneEvent.project)) {
+      return false;
+    }
+    if (!equals(expected.ref, actualRefReplicatedDoneEvent.ref)) {
+      return false;
+    }
+    if (expected.nodesCount != actualRefReplicatedDoneEvent.nodesCount) {
+      return false;
+    }
+    return true;
+  }
+
+  private static boolean equals(Object object1, Object object2) {
+    if (object1 == object2) {
+      return true;
+    }
+    if (object1 != null && !object1.equals(object2)){
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public void appendTo(StringBuffer buffer) {
+    buffer.append("eqEvent(");
+    buffer.append(expected.getClass().getName());
+    buffer.append(" with project \"");
+    buffer.append(expected.project);
+    buffer.append(" and ref \"");
+    buffer.append(expected.ref);
+    buffer.append(" and nodesCount \"");
+    buffer.append(expected.nodesCount);
+    buffer.append("\")");
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
new file mode 100644
index 0000000..a0bf576
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
@@ -0,0 +1,219 @@
+// Copyright (C) 2013 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 static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.resetToDefault;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
+
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.URISyntaxException;
+
+public class ReplicationStateTest {
+
+  private ReplicationState replicationState;
+  private PushResultProcessing pushResultProcessingMock;
+
+  @Before
+  public void setUp() throws Exception {
+    pushResultProcessingMock = createNiceMock(PushResultProcessing.class);
+    replay(pushResultProcessingMock);
+    replicationState = new ReplicationState(pushResultProcessingMock);
+  }
+
+  @Test
+  public void shouldNotHavePushTask() {
+    assertFalse(replicationState.hasPushTask());
+  }
+
+  @Test
+  public void shouldHavePushTask() {
+    replicationState.increasePushTaskCount("someProject", "someRef");
+    assertTrue(replicationState.hasPushTask());
+  }
+
+  @Test
+  public void shouldFireOneReplicationEventWhenNothingToReplicate() {
+    resetToDefault(pushResultProcessingMock);
+
+    //expected event
+    pushResultProcessingMock.onAllRefsReplicatedToAllNodes(0);
+    replay(pushResultProcessingMock);
+
+    //actual test
+    replicationState.markAllPushTasksScheduled();
+    verify(pushResultProcessingMock);
+  }
+
+  @Test
+  public void shouldFireEventsForReplicationOfOneRefToOneNode()
+      throws URISyntaxException {
+    resetToDefault(pushResultProcessingMock);
+    URIish uri = new URIish("git://someHost/someRepo.git");
+
+    //expected events
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
+        uri, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject",
+        "someRef", 1);
+    pushResultProcessingMock.onAllRefsReplicatedToAllNodes(1);
+    replay(pushResultProcessingMock);
+
+    //actual test
+    replicationState.increasePushTaskCount("someProject", "someRef");
+    replicationState.markAllPushTasksScheduled();
+    replicationState.notifyRefReplicated("someProject", "someRef", uri,
+        RefPushResult.SUCCEEDED);
+    verify(pushResultProcessingMock);
+  }
+
+  @Test
+  public void shouldFireEventsForReplicationOfOneRefToMultipleNodes()
+      throws URISyntaxException {
+    resetToDefault(pushResultProcessingMock);
+    URIish uri1 = new URIish("git://someHost1/someRepo.git");
+    URIish uri2 = new URIish("git://someHost2/someRepo.git");
+
+    //expected events
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
+        uri1, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
+        uri2, RefPushResult.FAILED);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("someProject",
+        "someRef", 2);
+    pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
+    replay(pushResultProcessingMock);
+
+    //actual test
+    replicationState.increasePushTaskCount("someProject", "someRef");
+    replicationState.increasePushTaskCount("someProject", "someRef");
+    replicationState.markAllPushTasksScheduled();
+    replicationState.notifyRefReplicated("someProject", "someRef", uri1,
+        RefPushResult.SUCCEEDED);
+    replicationState.notifyRefReplicated("someProject", "someRef", uri2,
+        RefPushResult.FAILED);
+    verify(pushResultProcessingMock);
+  }
+
+  @Test
+  public void shouldFireEventsForReplicationOfMultipleRefsToMultipleNodes()
+      throws URISyntaxException {
+    resetToDefault(pushResultProcessingMock);
+    URIish uri1 = new URIish("git://host1/someRepo.git");
+    URIish uri2 = new URIish("git://host2/someRepo.git");
+    URIish uri3 = new URIish("git://host3/someRepo.git");
+
+    //expected events
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+        uri1, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+        uri2, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+        uri3, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
+        uri1, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
+        uri2, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock
+        .onRefReplicatedToAllNodes("someProject", "ref1", 3);
+    pushResultProcessingMock
+        .onRefReplicatedToAllNodes("someProject", "ref2", 2);
+    pushResultProcessingMock.onAllRefsReplicatedToAllNodes(5);
+    replay(pushResultProcessingMock);
+
+    //actual test
+    replicationState.increasePushTaskCount("someProject", "ref1");
+    replicationState.increasePushTaskCount("someProject", "ref1");
+    replicationState.increasePushTaskCount("someProject", "ref1");
+    replicationState.increasePushTaskCount("someProject", "ref2");
+    replicationState.increasePushTaskCount("someProject", "ref2");
+    replicationState.markAllPushTasksScheduled();
+    replicationState.notifyRefReplicated("someProject", "ref1", uri1,
+        RefPushResult.SUCCEEDED);
+    replicationState.notifyRefReplicated("someProject", "ref1", uri2,
+        RefPushResult.SUCCEEDED);
+    replicationState.notifyRefReplicated("someProject", "ref1", uri3,
+        RefPushResult.SUCCEEDED);
+    replicationState.notifyRefReplicated("someProject", "ref2", uri1,
+        RefPushResult.SUCCEEDED);
+    replicationState.notifyRefReplicated("someProject", "ref2", uri2,
+        RefPushResult.SUCCEEDED);
+    verify(pushResultProcessingMock);
+  }
+
+  @Test
+  public void shouldFireEventsForReplicationSameRefDifferentProjects()
+      throws URISyntaxException {
+    resetToDefault(pushResultProcessingMock);
+    URIish uri = new URIish("git://host1/someRepo.git");
+
+    //expected events
+    pushResultProcessingMock.onRefReplicatedToOneNode("project1", "ref1", uri,
+        RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToOneNode("project2", "ref2", uri,
+        RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("project1", "ref1", 1);
+    pushResultProcessingMock.onRefReplicatedToAllNodes("project2", "ref2", 1);
+    pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
+    replay(pushResultProcessingMock);
+
+    //actual test
+    replicationState.increasePushTaskCount("project1", "ref1");
+    replicationState.increasePushTaskCount("project2", "ref2");
+    replicationState.markAllPushTasksScheduled();
+    replicationState.notifyRefReplicated("project1", "ref1", uri,
+        RefPushResult.SUCCEEDED);
+    replicationState.notifyRefReplicated("project2", "ref2", uri,
+        RefPushResult.SUCCEEDED);
+    verify(pushResultProcessingMock);
+  }
+
+  @Test
+  public void shouldFireEventsWhenSomeReplicationCompleteBeforeAllTasksAreScheduled()
+      throws URISyntaxException {
+    resetToDefault(pushResultProcessingMock);
+    URIish uri1 = new URIish("git://host1/someRepo.git");
+
+   //expected events
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+        uri1, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
+        uri1, RefPushResult.SUCCEEDED);
+    pushResultProcessingMock
+        .onRefReplicatedToAllNodes("someProject", "ref1", 1);
+    pushResultProcessingMock
+        .onRefReplicatedToAllNodes("someProject", "ref2", 1);
+    pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
+    replay(pushResultProcessingMock);
+
+    //actual test
+    replicationState.increasePushTaskCount("someProject", "ref1");
+    replicationState.increasePushTaskCount("someProject", "ref2");
+    replicationState.notifyRefReplicated("someProject", "ref1", uri1,
+        RefPushResult.SUCCEEDED);
+    replicationState.notifyRefReplicated("someProject", "ref2", uri1,
+        RefPushResult.SUCCEEDED);
+    replicationState.markAllPushTasksScheduled();
+    verify(pushResultProcessingMock);
+  }
+}