Merge branch 'stable-3.12'

* stable-3.12:
  Make excludedRefsPattern() to return an empty list by default
  Do not push deletes for refs configured to be skipped
  Add a remote config to exclude replicating desired refs
  Auto-format source code using gjf

Change-Id: I57e2f34aa6b3012d2aaca69ac13899ab2bc392c2
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 3d7ae36..d535934 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -85,6 +85,7 @@
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -890,6 +891,10 @@
     return config.replicateNoteDbMetaRefs();
   }
 
+  ImmutableList<Pattern> excludedRefsPattern() {
+    return config.excludedRefsPattern();
+  }
+
   private static boolean matches(URIish uri, String urlMatch) {
     if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
       return true;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
index 4d72ff0..41ab4c4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
@@ -20,8 +20,12 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.config.ConfigUtil;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.RemoteConfig;
 
@@ -51,6 +55,7 @@
   private final int maxRetries;
   private final int slowLatencyThreshold;
   private final Supplier<Integer> pushBatchSize;
+  private final ImmutableList<Pattern> excludedRefsPattern;
 
   protected DestinationConfiguration(RemoteConfig remoteConfig, Config cfg) {
     this.remoteConfig = remoteConfig;
@@ -116,6 +121,7 @@
               }
               return 0;
             });
+    excludedRefsPattern = getExcludedRefsPattern(cfg, name);
   }
 
   @Override
@@ -215,4 +221,21 @@
   public int getPushBatchSize() {
     return pushBatchSize.get();
   }
+
+  @Override
+  public ImmutableList<Pattern> excludedRefsPattern() {
+    return excludedRefsPattern;
+  }
+
+  private ImmutableList<Pattern> getExcludedRefsPattern(Config cfg, String name) {
+    List<Pattern> patterns = new ArrayList<>();
+    for (String regex : cfg.getStringList("remote", name, "excludedRefsPattern")) {
+      try {
+        patterns.add(Pattern.compile(regex));
+      } catch (PatternSyntaxException e) {
+        repLog.atWarning().log("Invalid excludedRefsPattern '%s' is ignored", regex);
+      }
+    }
+    return ImmutableList.copyOf(patterns);
+  }
 }
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 c2c2ed8..030ba1c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -739,8 +739,12 @@
           srcRef = git.exactRef(src);
         }
 
-        if (srcRef != null && canPushRef(src, noPerms)) {
-          push(git, cmds, spec, srcRef);
+        if (srcRef != null) {
+          if (canPushRef(src, noPerms)) {
+            push(git, cmds, spec, srcRef);
+          } else {
+            repLog.atFine().log("Skipping push of ref %s", srcRef.getName());
+          }
         } else if (config.isMirror()) {
           delete(git, cmds, spec);
         }
@@ -752,7 +756,8 @@
   private boolean canPushRef(String ref, boolean noPerms) {
     return !(noPerms && RefNames.REFS_CONFIG.equals(ref))
         && !ref.startsWith(RefNames.REFS_CACHE_AUTOMERGE)
-        && !(!pool.replicateNoteDbMetaRefs() && RefNames.isNoteDbMetaRef(ref));
+        && !(!pool.replicateNoteDbMetaRefs() && RefNames.isNoteDbMetaRef(ref))
+        && pool.excludedRefsPattern().stream().noneMatch(p -> p.matcher(ref).matches());
   }
 
   private Map<String, Ref> listRemote(Transport tn)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java
index e39afd6..67d23b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java
@@ -14,6 +14,7 @@
 package com.googlesource.gerrit.plugins.replication;
 
 import com.google.common.collect.ImmutableList;
+import java.util.regex.Pattern;
 import org.eclipse.jgit.transport.RemoteConfig;
 
 /** Remote configuration for a replication endpoint */
@@ -142,4 +143,13 @@
     }
     return ret;
   }
+
+  /**
+   * List of patterns that will be used to exclude refs from being replicated
+   *
+   * @return list of successfully compiled patterns
+   */
+  default ImmutableList<Pattern> excludedRefsPattern() {
+    return ImmutableList.of();
+  }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index bb4be9a..67c201c 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -604,6 +604,13 @@
 
   By default, true.
 
+remote.NAME.excludedRefsPattern
+: Refs that match the pattern provided using this config will not be replicated.
+  This option is useful when admins want to skip replicating certain refs, for
+  example refs created by plugins. Multiple excludedRefsPattern keys can be
+  supplied, to specify multiple patterns to match against.
+
+  Do not exclude any refs pattern by default.
 
 Directory `replication`
 --------------------
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java
index 47470df..339123a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java
@@ -53,6 +53,7 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.errors.TransportException;
@@ -75,6 +76,7 @@
 import org.eclipse.jgit.util.FS;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -294,6 +296,40 @@
   }
 
   @Test
+  public void skipPushingExcludedRefs() throws InterruptedException, IOException {
+    when(destinationMock.excludedRefsPattern())
+        .thenReturn(
+            ImmutableList.of(Pattern.compile("refs/foo/.*"), Pattern.compile("refs/bar/.*")));
+    PushOne pushOne = Mockito.spy(createPushOne(null));
+
+    Ref ref1 =
+        new ObjectIdRef.Unpeeled(
+            NEW,
+            "refs/heads/master",
+            ObjectId.fromString("0000000000000000000000000000000000000002"));
+    Ref ref2 =
+        new ObjectIdRef.Unpeeled(
+            NEW, "refs/foo/test", ObjectId.fromString("0000000000000000000000000000000000000003"));
+    Ref ref3 =
+        new ObjectIdRef.Unpeeled(
+            NEW, "refs/bar/test", ObjectId.fromString("0000000000000000000000000000000000000004"));
+
+    localRefs.add(ref1);
+    localRefs.add(ref2);
+    localRefs.add(ref3);
+
+    pushOne.addRefBatch(ImmutableSet.of(ref1.getName(), ref2.getName(), ref3.getName()));
+    pushOne.run();
+
+    isCallFinished.await(10, TimeUnit.SECONDS);
+
+    ArgumentCaptor<Ref> refCaptor = ArgumentCaptor.forClass(Ref.class);
+    verify(transportMock, atLeastOnce()).push(any(), any());
+    verify(pushOne, times(1)).push(any(), any(), any(), refCaptor.capture());
+    assertThat(refCaptor.getValue().getName()).isEqualTo("refs/heads/master");
+  }
+
+  @Test
   public void shouldNotAttemptDuplicateRemoteRefUpdate() throws InterruptedException, IOException {
     PushOne pushOne = Mockito.spy(createPushOne(null));
 
@@ -459,6 +495,7 @@
   private void setupDestinationMock() {
     destinationMock = mock(Destination.class);
     when(destinationMock.requestRunway(any())).thenReturn(RunwayStatus.allowed());
+    when(destinationMock.excludedRefsPattern()).thenReturn(ImmutableList.of());
   }
 
   private void setupPermissionBackedMock() {