Merge "SubmoduleOp: Allow all branches to be subscribed"
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index bccb074..7b5f0a6 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -86,7 +86,7 @@
 and add the following lines:
 ----
   [allowSuperproject "<superproject>"]
-    refs = <refspec>
+    matching = <refspec>
 ----
 where the 'superproject' should be the exact project name of the superproject.
 The refspec defines which branches of the submodule are allowed to be
@@ -104,7 +104,7 @@
 a configuration in the "All-Projects" project like:
 ----
     [allowSuperproject "my-only-superproject"]
-        refs = refs/heads/*:refs/heads/*
+        matching = refs/heads/*:refs/heads/*
 ----
 and then you don't have to worry about configuring the individual projects
 any more. Child projects cannot negate the parent's configuration.
@@ -147,14 +147,17 @@
 
 [[acl_refspec]]
 === The RefSpec in the allowSuperproject section
-The RefSpec for defining the branch level access for subscriptions look similar
-to Git style RefSpecs used for pushing in Git. Regular expressions
-as found in the ACL configuration are not supported. The most restrictive
-RefSpec is allowing one specific branch of the submodule to be subscribed
-to one specific branch of the superproject via:
+There are two options for specifying which branches can be subscribed
+to. The most common is to set `allowSuperproject.<superproject>.matching`
+to a Git-style refspec, which has the same syntax as the refspecs used
+for pushing in Git. Regular expressions as found in the ACL configuration
+are not supported.
+
+The most restrictive refspec is allowing one specific branch of the
+submodule to be subscribed to one specific branch of the superproject:
 ----
   [allowSuperproject "<superproject>"]
-    refs = refs/heads/<submodule-branch>:refs/heads/<superproject-branch>
+    matching = refs/heads/<submodule-branch>:refs/heads/<superproject-branch>
 ----
 
 If you want to allow for a 1:1 mapping, i.e. 'master' maps to 'master',
@@ -162,14 +165,24 @@
 'stable':
 ----
   [allowSuperproject "<superproject>"]
-    refs = refs/heads/*:refs/heads/*
+    matching = refs/heads/*:refs/heads/*
 ----
 
-If you want to enable a branch to be subscribed to any other branch of
-the superproject, omit the second part of the RefSpec:
+To allow all refs matching one pattern to subscribe to all refs
+matching another pattern, set `allowSuperproject.<superproject>.all`
+to the patterns concatenated with a colon. For example, to make a
+single branch available for subscription from all branches of the
+superproject:
 ----
   [allowSuperproject "<superproject>"]
-    refs = refs/heads/<submodule-branch>
+     all = refs/heads/<submodule-branch>:refs/heads/*
+----
+
+To make all branches available for subscription from all branches of
+the superproject:
+----
+  [allowSuperproject "<superproject>"]
+     all = refs/heads/*:refs/heads/*
 ----
 
 === Subscription Limitations
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 2ca4691..28f7ff8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -130,18 +130,25 @@
     return pushChangeTo(repo, "refs/heads/" + branch, "some change", "");
   }
 
-  protected void allowSubmoduleSubscription(String submodule, String subBranch,
-      String superproject, String superBranch) throws Exception {
+  protected void allowSubmoduleSubscription(String submodule,
+      String subBranch, String superproject, String superBranch, boolean match)
+      throws Exception {
     Project.NameKey sub = new Project.NameKey(name(submodule));
     Project.NameKey superName = new Project.NameKey(name(superproject));
     try (MetaDataUpdate md = metaDataUpdateFactory.create(sub)) {
       md.setMessage("Added superproject subscription");
       ProjectConfig pc = ProjectConfig.read(md);
       SubscribeSection s = new SubscribeSection(superName);
+      String refspec;
       if (superBranch == null) {
-        s.addRefSpec(subBranch);
+        refspec = subBranch;
       } else {
-        s.addRefSpec(subBranch + ":" + superBranch);
+        refspec = subBranch + ":" + superBranch;
+      }
+      if (match) {
+        s.addMatchingRefSpec(refspec);
+      } else {
+        s.addMultiMatchRefSpec(refspec);
       }
       pc.addSubscribeSection(s);
       ObjectId oldId = pc.getRevision();
@@ -151,6 +158,13 @@
     }
   }
 
+  protected void allowMatchingSubmoduleSubscription(String submodule,
+      String subBranch, String superproject, String superBranch)
+      throws Exception {
+    allowSubmoduleSubscription(submodule, subBranch, superproject,
+        superBranch, true);
+  }
+
   protected void createSubmoduleSubscription(TestRepository<?> repo, String branch,
       String subscribeToRepo, String subscribeToBranch) throws Exception {
     Config config = new Config();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 5326262..6684e85 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -44,7 +44,7 @@
   public void testSubscriptionWithoutGlobalServerSetting() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master",
@@ -70,7 +70,7 @@
   public void testSubscriptionToEmptyRepo() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master",
@@ -87,7 +87,7 @@
   public void testSubscriptionToExistingRepo() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -104,8 +104,8 @@
   public void testSubscriptionWildcardACLForSingleBranch() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    // master is allowed to be subscribed to any superprojects branch:
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    // master is allowed to be subscribed to master branch only:
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", null);
     // create 'branch':
     pushChangeTo(superRepo, "branch");
@@ -120,14 +120,14 @@
 
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
-    expectToHaveSubmoduleState(superRepo, "branch",
-        "subscribed-to-project", subHEAD);
+    assertThat(hasSubmodule(superRepo, "branch",
+        "subscribed-to-project")).isFalse();
   }
 
   @Test
   public void testSubscriptionWildcardACLForMissingProject() throws Exception {
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "not-existing-super-project", "refs/heads/*");
     pushChangeTo(subRepo, "master");
   }
@@ -136,7 +136,7 @@
   public void testSubscriptionWildcardACLForMissingBranch() throws Exception {
     createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "super-project", "refs/heads/*");
     pushChangeTo(subRepo, "foo");
   }
@@ -145,7 +145,7 @@
   public void testSubscriptionWildcardACLForMissingGitmodules() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "super-project", "refs/heads/*");
     pushChangeTo(superRepo, "master");
     pushChangeTo(subRepo, "master");
@@ -156,7 +156,7 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
     // any branch is allowed to be subscribed to the same superprojects branch:
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
         "super-project", "refs/heads/*");
 
     // create 'branch' in both repos:
@@ -189,11 +189,52 @@
   }
 
   @Test
+  public void testSubscriptionWildcardACLForManyBranches() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    // Any branch is allowed to be subscribed to any superproject branch:
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+        "super-project", null, false);
+    pushChangeTo(superRepo, "branch");
+    pushChangeTo(subRepo, "another-branch");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "another-branch");
+    ObjectId subHEAD = pushChangeTo(subRepo, "another-branch");
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD);
+  }
+
+  @Test
+  public void testSubscriptionWildcardACLOneToManyBranches() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    // Any branch is allowed to be subscribed to any superproject branch:
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/*", false);
+    pushChangeTo(superRepo, "branch");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "master");
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD);
+
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "branch");
+    pushChangeTo(subRepo, "branch");
+
+    // no change expected, as only master is subscribed:
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD);
+  }
+
+  @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "false")
   public void testSubmoduleShortCommitMessage() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -220,7 +261,7 @@
   public void testSubmoduleSubjectCommitMessage() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -248,7 +289,7 @@
   public void testSubmoduleCommitMessage() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -276,7 +317,7 @@
   public void testSubscriptionUnsubscribe() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -303,7 +344,7 @@
       throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -329,7 +370,7 @@
   public void testSubscriptionToDifferentBranches() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/foo",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/foo",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master",
@@ -345,9 +386,9 @@
   public void testBranchCircularSubscription() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("super-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("super-project", "refs/heads/master",
         "subscribed-to-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -370,9 +411,9 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("super-project", "refs/heads/dev",
+    allowMatchingSubmoduleSubscription("super-project", "refs/heads/dev",
         "subscribed-to-project", "refs/heads/dev");
 
     pushChangeTo(subRepo, "master");
@@ -414,7 +455,7 @@
   public void testSubscriptionFailOnWrongProjectACL() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "wrong-super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -429,7 +470,7 @@
   public void testSubscriptionFailOnWrongBranchACL() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/wrong-branch");
 
     pushChangeTo(subRepo, "master");
@@ -448,7 +489,7 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project",
         new Project.NameKey(name("config-repo2")));
-    allowSubmoduleSubscription("config-repo", "refs/heads/*",
+    allowMatchingSubmoduleSubscription("config-repo", "refs/heads/*",
         "super-project", "refs/heads/*");
 
     pushChangeTo(subRepo, "master");
@@ -463,7 +504,7 @@
   public void testAllowedButNotSubscribed() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
@@ -490,7 +531,7 @@
     TestRepository<?> subRepo = createProjectWithPush(
         "nested/subscribed-to-project");
     // master is allowed to be subscribed to any superprojects branch:
-    allowSubmoduleSubscription("nested/subscribed-to-project",
+    allowMatchingSubmoduleSubscription("nested/subscribed-to-project",
         "refs/heads/master", "super-project", null);
 
     pushChangeTo(subRepo, "master");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index dfce62e..0ff3af5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -58,7 +58,7 @@
   public void testSubscriptionUpdateOfManyChanges() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -113,7 +113,7 @@
   public void testSubscriptionUpdateIncludingChangeInSuperproject() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -181,11 +181,11 @@
     TestRepository<?> sub2 = createProjectWithPush("sub2");
     TestRepository<?> sub3 = createProjectWithPush("sub3");
 
-    allowSubmoduleSubscription("sub1", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub1", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("sub2", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub2", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("sub3", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub3", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     Config config = new Config();
@@ -227,7 +227,7 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> sub = createProjectWithPush("sub");
 
-    allowSubmoduleSubscription("sub", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     Config config = new Config();
@@ -261,7 +261,7 @@
     TestRepository<?> sub = createProjectWithPush("sub");
     TestRepository<?> standAlone = createProjectWithPush("standalone");
 
-    allowSubmoduleSubscription("sub", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("sub", "refs/heads/master",
         "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master", "sub", "master");
@@ -301,9 +301,9 @@
     TestRepository<?> midRepo = createProjectWithPush("mid-project");
     TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
 
-    allowSubmoduleSubscription("mid-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("mid-project", "refs/heads/master",
         "top-project", "refs/heads/master");
-    allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
         "mid-project", "refs/heads/master");
 
     createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
@@ -332,11 +332,11 @@
     TestRepository<?> midRepo = createProjectWithPush("mid-project");
     TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
 
-    allowSubmoduleSubscription("mid-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("mid-project", "refs/heads/master",
         "top-project", "refs/heads/master");
-    allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
         "mid-project", "refs/heads/master");
-    allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
         "top-project", "refs/heads/master");
 
     createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
@@ -373,11 +373,11 @@
     createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
     createSubmoduleSubscription(bottomRepo, "master", "top-project", "master");
 
-    allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("bottom-project", "refs/heads/master",
         "mid-project", "refs/heads/master");
-    allowSubmoduleSubscription("mid-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("mid-project", "refs/heads/master",
         "top-project", "refs/heads/master");
-    allowSubmoduleSubscription("top-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("top-project", "refs/heads/master",
         "bottom-project", "refs/heads/master");
 
     ObjectId bottomMasterHead =
@@ -401,9 +401,9 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+    allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
-    allowSubmoduleSubscription("super-project", "refs/heads/dev",
+    allowMatchingSubmoduleSubscription("super-project", "refs/heads/dev",
         "subscribed-to-project", "refs/heads/dev");
 
     pushChangeTo(subRepo, "dev");
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
index 6cee630..3fdc331 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -29,20 +29,28 @@
 @GwtIncompatible("Unemulated org.eclipse.jgit.transport.RefSpec")
 public class SubscribeSection {
 
-  private final List<RefSpec> refSpecs;
+  private final List<RefSpec> multiMatchRefSpecs;
+  private final List<RefSpec> matchingRefSpecs;
   private final Project.NameKey project;
 
   public SubscribeSection(Project.NameKey p) {
     project = p;
-    refSpecs = new ArrayList<>();
+    matchingRefSpecs = new ArrayList<>();
+    multiMatchRefSpecs = new ArrayList<>();
   }
 
-  public void addRefSpec(RefSpec spec) {
-    refSpecs.add(spec);
+  public void addMatchingRefSpec(RefSpec spec) {
+    matchingRefSpecs.add(spec);
   }
 
-  public void addRefSpec(String spec) {
-    refSpecs.add(new RefSpec(spec));
+  public void addMatchingRefSpec(String spec) {
+    RefSpec r = new RefSpec(spec);
+    matchingRefSpecs.add(r);
+  }
+
+  public void addMultiMatchRefSpec(String spec) {
+    RefSpec r = new RefSpec(spec, RefSpec.WildcardMode.ALLOW_MISMATCH);
+    multiMatchRefSpecs.add(r);
   }
 
   public Project.NameKey getProject() {
@@ -57,7 +65,12 @@
    * @return if the branch could trigger a superproject update
    */
   public boolean appliesTo(Branch.NameKey branch) {
-    for (RefSpec r : refSpecs) {
+    for (RefSpec r : matchingRefSpecs) {
+      if (r.matchSource(branch.get())) {
+        return true;
+      }
+    }
+    for (RefSpec r : multiMatchRefSpecs) {
       if (r.matchSource(branch.get())) {
         return true;
       }
@@ -65,8 +78,12 @@
     return false;
   }
 
-  public Collection<RefSpec> getRefSpecs() {
-    return Collections.unmodifiableCollection(refSpecs);
+  public Collection<RefSpec> getMatchingRefSpecs() {
+    return Collections.unmodifiableCollection(matchingRefSpecs);
+  }
+
+  public Collection<RefSpec> getMultiMatchRefSpecs() {
+    return Collections.unmodifiableCollection(multiMatchRefSpecs);
   }
 
   @Override
@@ -74,10 +91,19 @@
     StringBuilder ret = new StringBuilder();
     ret.append("[SubscribeSection, project=");
     ret.append(project);
-    ret.append(", refs=[");
-    for (RefSpec r : refSpecs) {
-      ret.append(r.toString());
-      ret.append(", ");
+    if (!matchingRefSpecs.isEmpty()) {
+      ret.append(", matching=[");
+      for (RefSpec r : matchingRefSpecs) {
+        ret.append(r.toString());
+        ret.append(", ");
+      }
+    }
+    if (!multiMatchRefSpecs.isEmpty()) {
+      ret.append(", all=[");
+      for (RefSpec r : multiMatchRefSpecs) {
+        ret.append(r.toString());
+        ret.append(", ");
+      }
     }
     ret.append("]");
     return ret.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 6d9d759..64d9a9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -132,7 +132,8 @@
   private static final String KEY_STATE = "state";
 
   private static final String SUBSCRIBE_SECTION = "allowSuperproject";
-  private static final String SUBSCRIBE_REFS = "refs";
+  private static final String SUBSCRIBE_MATCH_REFS = "matching";
+  private static final String SUBSCRIBE_MULTI_MATCH_REFS = "all";
 
   private static final String DASHBOARD = "dashboard";
   private static final String KEY_DEFAULT = "default";
@@ -848,8 +849,12 @@
         Project.NameKey p = new Project.NameKey(projectName);
         SubscribeSection ss = new SubscribeSection(p);
         for (String s : rc.getStringList(SUBSCRIBE_SECTION,
-            projectName, SUBSCRIBE_REFS)) {
-          ss.addRefSpec(s);
+            projectName, SUBSCRIBE_MULTI_MATCH_REFS)) {
+          ss.addMultiMatchRefSpec(s);
+        }
+        for (String s : rc.getStringList(SUBSCRIBE_SECTION,
+            projectName, SUBSCRIBE_MATCH_REFS)) {
+          ss.addMatchingRefSpec(s);
         }
         subscribeSections.put(p, ss);
       }
@@ -1238,8 +1243,13 @@
   private void saveSubscribeSections(Config rc) {
     for (Project.NameKey p : subscribeSections.keySet()) {
       SubscribeSection s = subscribeSections.get(p);
-      for (RefSpec r : s.getRefSpecs()) {
-        rc.setString(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_REFS, r.toString());
+      for (RefSpec r : s.getMatchingRefSpecs()) {
+        rc.setString(SUBSCRIBE_SECTION, p.get(),
+            SUBSCRIBE_MATCH_REFS, r.toString());
+      }
+      for (RefSpec r : s.getMultiMatchRefSpecs()) {
+        rc.setString(SUBSCRIBE_SECTION, p.get(),
+            SUBSCRIBE_MULTI_MATCH_REFS, r.toString());
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index b23ff95..76f74ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -62,6 +62,7 @@
 import java.util.Collections;
 import java.util.Deque;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Objects;
@@ -222,33 +223,50 @@
 
   private Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
       SubscribeSection s) throws IOException {
-    Collection<Branch.NameKey> ret = new ArrayList<>();
+    Collection<Branch.NameKey> ret = new HashSet<>();
     logDebug("Inspecting SubscribeSection " + s);
-    for (RefSpec r : s.getRefSpecs()) {
-      logDebug("Inspecting ref " + r);
-      if (r.matchSource(src.get())) {
-        if (r.getDestination() == null) {
-          // no need to care for wildcard, as we matched already
-          OpenRepo or;
-          try {
-            or = orm.openRepo(s.getProject(), false);
-          } catch (NoSuchProjectException e) {
-            // A project listed a non existent project to be allowed
-            // to subscribe to it. Allow this for now.
-            continue;
-          }
+    for (RefSpec r : s.getMatchingRefSpecs()) {
+      logDebug("Inspecting [matching] ref " + r);
+      if (!r.matchSource(src.get())) {
+        continue;
+      }
+      if (r.isWildcard()) {
+        // refs/heads/*[:refs/somewhere/*]
+        ret.add(new Branch.NameKey(s.getProject(),
+            r.expandFromSource(src.get()).getDestination()));
+      } else {
+        // e.g. refs/heads/master[:refs/heads/stable]
+        String dest = r.getDestination();
+        if (dest == null) {
+          dest = r.getSource();
+        }
+        ret.add(new Branch.NameKey(s.getProject(), dest));
+      }
+    }
 
-          for (Ref ref : or.repo.getRefDatabase().getRefs(
-              RefNames.REFS_HEADS).values()) {
-            ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
-          }
-        } else if (r.isWildcard()) {
-          // refs/heads/*:refs/heads/*
-          ret.add(new Branch.NameKey(s.getProject(),
-              r.expandFromSource(src.get()).getDestination()));
-        } else {
-          // e.g. refs/heads/master:refs/heads/stable
-          ret.add(new Branch.NameKey(s.getProject(), r.getDestination()));
+    for (RefSpec r : s.getMultiMatchRefSpecs()) {
+      logDebug("Inspecting [all] ref " + r);
+      if (!r.matchSource(src.get())) {
+        continue;
+      }
+      OpenRepo or;
+      try {
+        or = orm.openRepo(s.getProject(), false);
+      } catch (NoSuchProjectException e) {
+        // A project listed a non existent project to be allowed
+        // to subscribe to it. Allow this for now, i.e. no exception is
+        // thrown.
+        continue;
+      }
+
+      for (Ref ref : or.repo.getRefDatabase().getRefs(
+          RefNames.REFS_HEADS).values()) {
+        if (r.getDestination() != null && !r.matchDestination(ref.getName())) {
+          continue;
+        }
+        Branch.NameKey b = new Branch.NameKey(s.getProject(), ref.getName());
+        if (!ret.contains(b)) {
+          ret.add(b);
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
index e95353a..fc1b0cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
@@ -79,10 +79,10 @@
         }
         RefSpec newRefSpec = new RefSpec(subbranch.get() + ":" + superBranch.get());
 
-        if (!s.getRefSpecs().contains(newRefSpec)) {
+        if (!s.getMatchingRefSpecs().contains(newRefSpec)) {
           // For the migration we use only exact RefSpecs, we're not trying to
           // generalize it.
-          s.addRefSpec(newRefSpec);
+          s.addMatchingRefSpec(newRefSpec);
         }
 
         pc.commit(md);