Support exclusion of ref patterns

Currently the plugin can only exclude individual refs. Support excluding
ref patterns for cases like automation creating refs/heads/auto-<uuid>
refs in the manifest repository.

Define the patterns on a git-style (only one '*' in the name). It is
consistent with other patterns in the conf and allows us to use the
RefSpec class for the matching.

Change-Id: I2de497ec2d276604230cf2430812a3776a27b58a
diff --git a/java/Documentation/about.md b/java/Documentation/about.md
index 54a5d34..95152c6 100644
--- a/java/Documentation/about.md
+++ b/java/Documentation/about.md
@@ -50,20 +50,22 @@
 
 For the destination branch, you may also specify `refs/heads/*` to copy all
 branches in the manifest repository. In this case the `srcRef` field is not
-required (it will be ignored if present). Specific branches can be excluded with
-the `exclude` option. `exclude` value is a comma-separated list of fully
-qualified refs (i.e. with the `refs/heads/` prefix).
+required (it will be ignored if present).
+
+Branches that match the srcRef can still be skipped with the `exclude`
+option. Its value is a comma-separated list of refs or ref patterns. Both of
+them fully qualified (i.e. with the `refs/heads/` prefix). Note that git ref
+patterns allow only one '*'.
 
 ```
 [superproject "submodules:refs/heads/*"]
    srcRepo = platforms/manifest
    srcPath = manifest.xml
-   exclude = refs/heads/test,refs/heads/ignoreme
+   exclude = refs/heads/ignoreme, refs/heads/*-release, refs/heads/auto-*
 ```
 
-This plugin bypasses visibility restrictions, so edits to the manifest
-repo can be used to reveal existence of hidden repositories or
-branches.
+This plugin bypasses visibility restrictions, so edits to the manifest repo can
+be used to reveal existence of hidden repositories or branches.
 
 
 MANUAL TRIGGER
diff --git a/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java b/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
index 09ac53c..681063f 100644
--- a/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
+++ b/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
@@ -22,11 +22,12 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RefSpec.WildcardMode;
 
 public class ConfigEntry {
   public static final String SECTION_NAME = "superproject";
@@ -108,6 +109,7 @@
     srcRefsExcluded =
         Stream.of(nullToEmpty(cfg.getString(SECTION_NAME, name, "exclude")).split(","))
             .map(String::trim)
+            .filter(s -> !s.isEmpty())
             .collect(ImmutableSet.toImmutableSet());
 
     xmlPath = cfg.getString(SECTION_NAME, name, "srcPath");
@@ -209,13 +211,15 @@
     return destBranch;
   }
 
-  /**
-   * Refs that should not be copied
-   *
-   * @return the refs listed in the "exclude" option
-   */
-  public Set<String> getSrcRefsExcluded() {
-    return srcRefsExcluded;
+  public boolean excludesRef(String refName) {
+    for (String excluded : srcRefsExcluded) {
+      // ALLOW_MISMATCH allows '*' only in one side (source in this case)
+      RefSpec excludedSpec = new RefSpec(excluded, WildcardMode.ALLOW_MISMATCH);
+      if (excludedSpec.matchSource(refName)) {
+        return true;
+      }
+    }
+    return false;
   }
 
   enum ToolType {
diff --git a/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java b/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
index bfaca2e..fa3d400 100644
--- a/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
+++ b/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
@@ -390,7 +390,8 @@
         continue;
       }
 
-      if (c.srcRefsExcluded.contains(refName)) {
+      if (c.excludesRef(refName)) {
+        info("Skipping %s: it matches exclude conditions.", refName);
         continue;
       }
       relevantConfigs.add(c);
diff --git a/javatests/com/googlesource/gerrit/plugins/supermanifest/ConfigEntryTest.java b/javatests/com/googlesource/gerrit/plugins/supermanifest/ConfigEntryTest.java
index 7017680..19e006d 100644
--- a/javatests/com/googlesource/gerrit/plugins/supermanifest/ConfigEntryTest.java
+++ b/javatests/com/googlesource/gerrit/plugins/supermanifest/ConfigEntryTest.java
@@ -44,6 +44,7 @@
     assertThat(entry.getSrcRepoKey()).isEqualTo(Project.nameKey("manifest"));
     assertThat(entry.getSrcRef()).isEqualTo("refs/heads/nyc-src");
     assertThat(entry.getXmlPath()).isEqualTo("default.xml");
+    assertThat(entry.excludesRef("refs/heads/master")).isFalse();
   }
 
   @Test
@@ -135,9 +136,11 @@
     cfg.fromText(builder.toString());
 
     ConfigEntry entry = new ConfigEntry(cfg, "superproject:refs/heads/*");
-
-    assertThat(entry.srcRefsExcluded)
-        .containsExactly("refs/heads/a", "refs/heads/b", "refs/heads/c");
+    assertThat(entry.excludesRef("refs/heads/a")).isTrue();
+    assertThat(entry.excludesRef("refs/heads/b")).isTrue();
+    assertThat(entry.excludesRef("refs/heads/c")).isTrue();
+    assertThat(entry.excludesRef("refs/tags/r1")).isFalse();
+    assertThat(entry.excludesRef("refs/heads/aaa")).isFalse();
   }
 
   @Test
@@ -155,9 +158,39 @@
     cfg.fromText(builder.toString());
 
     ConfigEntry entry = new ConfigEntry(cfg, "superproject:refs/heads/*");
+    assertThat(entry.excludesRef("refs/heads/a")).isTrue();
+    assertThat(entry.excludesRef("refs/heads/b")).isTrue();
+    assertThat(entry.excludesRef("refs/heads/c")).isTrue();
+    assertThat(entry.excludesRef("refs/tags/r1")).isFalse();
+    assertThat(entry.excludesRef("refs/heads/aaa")).isFalse();
+  }
 
-    assertThat(entry.srcRefsExcluded)
-        .containsExactly("refs/heads/a", "refs/heads/b", "refs/heads/c");
+  @Test
+  public void excluded_patterns() throws ConfigInvalidException {
+    StringBuilder builder =
+        new StringBuilder(
+                getBasicConf(
+                    "superproject",
+                    "refs/heads/*",
+                    "manifest",
+                    "refs/heads/nyc-src",
+                    "default.xml"))
+            .append("  exclude = refs/heads/a*, refs/heads/*-release \n");
+    Config cfg = new Config();
+    cfg.fromText(builder.toString());
+
+    ConfigEntry entry = new ConfigEntry(cfg, "superproject:refs/heads/*");
+    // Excluded
+    assertThat(entry.excludesRef("refs/heads/aa")).isTrue();
+    assertThat(entry.excludesRef("refs/heads/a-something")).isTrue();
+    assertThat(entry.excludesRef("refs/heads/b-release")).isTrue();
+    assertThat(entry.excludesRef("refs/heads/bb-release")).isTrue();
+
+    // Non excluded
+    assertThat(entry.excludesRef("refs/heads/a")).isFalse();
+    assertThat(entry.excludesRef("refs/heads/master")).isFalse();
+    assertThat(entry.excludesRef("refs/heads/c-release-c")).isFalse();
+    assertThat(entry.excludesRef("refs/tags/aa")).isFalse();
   }
 
   private String getBasicConf(