Merge branch 'stable-3.0' into stable-3.1

* stable-3.0:
  PushOne: Improve format of log of references to be pushed
  Don't lose state when there's a pending push to the same ref

Change-Id: I7d5ecbedad8f061c0f6d44b38ba18322bea19c78
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 029dc83..85c3917 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -428,7 +428,7 @@
         ScheduledFuture<?> ignored =
             pool.schedule(task, now ? 0 : config.getDelay(), TimeUnit.SECONDS);
         pending.put(uri, task);
-      } else if (!task.getRefs().contains(ref)) {
+      } else {
         addRef(task, ref);
         task.addState(ref, 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 2cbd627..b5838e5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -17,8 +17,10 @@
 import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toMap;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Throwables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -63,6 +65,7 @@
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.CredentialsProvider;
@@ -232,8 +235,7 @@
       delta.clear();
       pushAllRefs = true;
       repLog.trace("Added all refs for replication to {}", uri);
-    } else if (!pushAllRefs) {
-      delta.add(ref);
+    } else if (!pushAllRefs && delta.add(ref)) {
       repLog.trace("Added ref {} for replication to {}", ref, uri);
     }
   }
@@ -474,19 +476,46 @@
     }
 
     if (replConfig.getMaxRefsToLog() == 0 || todo.size() <= replConfig.getMaxRefsToLog()) {
-      repLog.info("Push to {} references: {}", uri, todo);
+      repLog.info("Push to {} references: {}", uri, refUpdatesForLogging(todo));
     } else {
       repLog.info(
           "Push to {} references (first {} of {} listed): {}",
           uri,
           replConfig.getMaxRefsToLog(),
           todo.size(),
-          todo.subList(0, replConfig.getMaxRefsToLog()));
+          refUpdatesForLogging(todo.subList(0, replConfig.getMaxRefsToLog())));
     }
 
     return tn.push(NullProgressMonitor.INSTANCE, todo);
   }
 
+  private static String refUpdatesForLogging(List<RemoteRefUpdate> refUpdates) {
+    return refUpdates.stream().map(PushOne::refUpdateForLogging).collect(joining(", "));
+  }
+
+  private static String refUpdateForLogging(RemoteRefUpdate update) {
+    String refSpec = String.format("%s:%s", update.getSrcRef(), update.getRemoteName());
+    String id =
+        String.format(
+            "%s..%s", objectIdToString(update.getExpectedOldObjectId()), update.getNewObjectId());
+    return MoreObjects.toStringHelper(RemoteRefUpdate.class)
+        .add("refSpec", refSpec)
+        .add("status", update.getStatus())
+        .add("id", id)
+        .add("force", booleanToString(update.isForceUpdate()))
+        .add("delete", booleanToString(update.isDelete()))
+        .add("ffwd", booleanToString(update.isFastForward()))
+        .toString();
+  }
+
+  private static String objectIdToString(ObjectId id) {
+    return id != null ? id.getName() : "(null)";
+  }
+
+  private static String booleanToString(boolean b) {
+    return b ? "yes" : "no";
+  }
+
   private List<RemoteRefUpdate> generateUpdates(Transport tn)
       throws IOException, PermissionBackendException {
     ProjectState projectState = projectCache.checkedGet(projectName);