Merge "Support wildcard matching in repository configuration"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 742d996..61c764a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2844,9 +2844,22 @@
   ownerGroup = Registered Users
 ----
 
-[NOTE]
-Currently only the repository name `*` is supported.
-This is a wildcard designating all repositories.
+The only matching patterns supported are exact match or wildcard matching which
+can be specified by ending the name with a `*`. If a project matches more than one
+repository configuration, then the configuration from the more precise match
+will be used. In the following example, the default submit type for a project
+named `project/plugins/a` would be `CHERRY_PICK`.
+
+----
+[repository "project/*"]
+  defaultSubmitType = MERGE_IF_NECESSARY
+[repository "project/plugins/*"]
+  defaultSubmitType = CHERRY_PICK
+----
+
+[NOTE] All properties are used from the matching repository configuration. In
+the previous example, all properties will be used from `project/plugins/\*`
+section and no properties will be inherited nor overridden from `project/*`.
 
 [[repository.name.defaultSubmitType]]repository.<name>.defaultSubmitType::
 +
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 6e4e2ed..99ff581 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
 import com.google.gerrit.extensions.webui.TopMenu;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AnonymousUser;
@@ -138,7 +137,6 @@
 import org.eclipse.jgit.transport.PreUploadHook;
 
 import java.util.List;
-import java.util.Set;
 
 
 /** Starts global state with standard dependencies. */
@@ -204,9 +202,8 @@
     bind(AccountVisibility.class)
         .toProvider(AccountVisibilityProvider.class)
         .in(SINGLETON);
-    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
-        .annotatedWith(ProjectOwnerGroups.class)
-        .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON);
+    factory(ProjectOwnerGroupsProvider.Factory.class);
+    bind(RepositoryConfig.class);
 
     bind(AuthBackend.class).to(UniversalAuthBackend.class).in(SINGLETON);
     DynamicSet.setOf(binder(), AuthBackend.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 5e2f71f..5dd2784 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -30,7 +30,8 @@
       @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
       ServerRequestContext serverCtx) {
-    super(gb, config, threadContext, serverCtx, "receive", null, "allowGroup");
+    super(gb, threadContext, serverCtx, config.getStringList("receive", null,
+        "allowGroup"));
 
     // If no group was set, default to "registered users"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index 79cfd88..545f48b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -29,7 +29,8 @@
       @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
       ServerRequestContext serverCtx) {
-    super(gb, config, threadContext, serverCtx, "upload", null, "allowGroup");
+    super(gb, threadContext, serverCtx, config.getStringList("upload", null,
+        "allowGroup"));
 
     // If no group was set, default to "registered users" and "anonymous"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 5c3ec39..9e55a7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -25,7 +25,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -40,13 +39,10 @@
 
   @Inject
   protected GroupSetProvider(GroupBackend groupBackend,
-      @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
-      ServerRequestContext serverCtx, String section,
-      String subsection, String name) {
+      ServerRequestContext serverCtx, String[] groupNames) {
     RequestContext ctx = threadContext.setContext(serverCtx);
     try {
-      String[] groupNames = config.getStringList(section, subsection, name);
       ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
       for (String n : groupNames) {
         GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
deleted file mode 100644
index 876c51f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2010 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.google.gerrit.server.config;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.inject.BindingAnnotation;
-
-import java.lang.annotation.Retention;
-
-/**
- * Marker on a {@code Set&lt;AccountGroup.Id>} for the configured groups which
- * should become owners of a created project.
- */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface ProjectOwnerGroups {
-}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 0189de3..23615d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,30 +14,37 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.util.ServerRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.Config;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 /**
  * Provider of the group(s) which should become owners of a newly created
- * project. Currently only supports {@code ownerGroup} declarations in the
- * {@code "*"} repository, like so:
+ * project. The only matching patterns supported are exact match or wildcard
+ * matching which can be specified by ending the name with a {@code *}.
  *
  * <pre>
  * [repository &quot;*&quot;]
  *     ownerGroup = Registered Users
  *     ownerGroup = Administrators
+ * [repository &quot;project/*&quot;]
+ *     ownerGroup = Administrators
  * </pre>
  */
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
-  @Inject
+
+  public interface Factory {
+    public ProjectOwnerGroupsProvider create(Project.NameKey project);
+  }
+
+  @AssistedInject
   public ProjectOwnerGroupsProvider(GroupBackend gb,
-      @GerritServerConfig final Config config,
-      ThreadLocalRequestContext context,
-      ServerRequestContext serverCtx) {
-    super(gb, config, context, serverCtx, "repository", "*", "ownerGroup");
+      ThreadLocalRequestContext context, ServerRequestContext serverCtx,
+      RepositoryConfig repositoryCfg,
+      @Assisted Project.NameKey project) {
+    super(gb, context, serverCtx, repositoryCfg.getOwnerGroups(project));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java
new file mode 100644
index 0000000..c34b8a6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2014 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.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class RepositoryConfig {
+
+  static final String SECTION_NAME = "repository";
+  static final String OWNER_GROUP_NAME = "ownerGroup";
+  static final String DEFAULT_SUBMIT_TYPE_NAME = "defaultSubmitType";
+
+  private final Config cfg;
+
+  @Inject
+  public RepositoryConfig(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
+  }
+
+  public SubmitType getDefaultSubmitType(Project.NameKey project) {
+    return cfg.getEnum(SECTION_NAME, findSubSection(project.get()),
+        DEFAULT_SUBMIT_TYPE_NAME, SubmitType.MERGE_IF_NECESSARY);
+  }
+
+  public String[] getOwnerGroups(Project.NameKey project) {
+    return cfg.getStringList(SECTION_NAME, findSubSection(project.get()),
+        OWNER_GROUP_NAME);
+  }
+
+  /**
+   * Find the subSection to get repository configuration from.
+   * <p>
+   * SubSection can use the * pattern so if project name matches more than one
+   * section, return the more precise one. E.g if the following subSections are
+   * defined:
+   *
+   * <pre>
+   * [repository "somePath/*"]
+   *   name = value
+   * [repository "somePath/somePath/*"]
+   *   name = value
+   * </pre>
+   *
+   * and this method is called with "somePath/somePath/someProject" as project
+   * name, it will return the subSection "somePath/somePath/*"
+   *
+   * @param project Name of the project
+   * @return the name of the subSection, null if none is found
+   */
+  private String findSubSection(String project) {
+    String subSectionFound = null;
+    for (String subSection : cfg.getSubsections(SECTION_NAME)) {
+      if (isMatch(subSection, project)
+          && (subSectionFound == null || subSectionFound.length() < subSection
+              .length())) {
+        subSectionFound = subSection;
+      }
+    }
+    return subSectionFound;
+  }
+
+  private boolean isMatch(String subSection, String project) {
+    return project.equals(subSection)
+        || (subSection.endsWith("*") && project.startsWith(subSection
+            .substring(0, subSection.length() - 1)));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index ee2e564..5b159fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -44,8 +44,8 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.ProjectOwnerGroups;
+import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
+import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -61,7 +61,6 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -77,7 +76,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
 public class CreateProject implements RestModifyView<TopLevelResource, ProjectInput> {
@@ -97,10 +95,10 @@
   private final DynamicSet<NewProjectCreatedListener> createdListener;
   private final ProjectCache projectCache;
   private final GroupBackend groupBackend;
-  private final Set<AccountGroup.UUID> projectOwnerGroups;
+  private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups;
   private final MetaDataUpdate.User metaDataUpdateFactory;
   private final GitReferenceUpdated referenceUpdated;
-  private final Config cfg;
+  private final RepositoryConfig repositoryCfg;
   private final PersonIdent serverIdent;
   private final Provider<CurrentUser> currentUser;
   private final Provider<PutConfig> putConfig;
@@ -115,10 +113,10 @@
       DynamicSet<NewProjectCreatedListener> createdListener,
       ProjectCache projectCache,
       GroupBackend groupBackend,
-      @ProjectOwnerGroups Set<AccountGroup.UUID> projectOwnerGroups,
+      ProjectOwnerGroupsProvider.Factory projectOwnerGroups,
       MetaDataUpdate.User metaDataUpdateFactory,
       GitReferenceUpdated referenceUpdated,
-      @GerritServerConfig Config cfg,
+      RepositoryConfig repositoryCfg,
       @GerritPersonIdent PersonIdent serverIdent,
       Provider<CurrentUser> currentUser,
       Provider<PutConfig> putConfig,
@@ -135,7 +133,7 @@
     this.projectOwnerGroups = projectOwnerGroups;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.referenceUpdated = referenceUpdated;
-    this.cfg = cfg;
+    this.repositoryCfg = repositoryCfg;
     this.serverIdent = serverIdent;
     this.currentUser = currentUser;
     this.putConfig = putConfig;
@@ -166,7 +164,8 @@
     args.submitType = input.submitType;
     args.branch = normalizeBranchNames(input.branches);
     if (input.owners == null || input.owners.isEmpty()) {
-      args.ownerIds = new ArrayList<>(projectOwnerGroups);
+      args.ownerIds =
+          new ArrayList<>(projectOwnerGroups.create(args.getProject()).get());
     } else {
       args.ownerIds =
         Lists.newArrayListWithCapacity(input.owners.size());
@@ -306,7 +305,7 @@
       Project newProject = config.getProject();
       newProject.setDescription(args.projectDescription);
       newProject.setSubmitType(MoreObjects.firstNonNull(args.submitType,
-          cfg.getEnum("repository", "*", "defaultSubmitType", SubmitType.MERGE_IF_NECESSARY)));
+          repositoryCfg.getDefaultSubmitType(args.getProject())));
       newProject
           .setUseContributorAgreements(args.contributorAgreements);
       newProject.setUseSignedOffBy(args.signedOffBy);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
new file mode 100644
index 0000000..1fb6d81
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2014 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.google.gerrit.server.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class RepositoryConfigTest {
+
+  private Config cfg;
+  private RepositoryConfig repoCfg;
+
+  @Before
+  public void setUp() throws Exception {
+    cfg = new Config();
+    repoCfg = new RepositoryConfig(cfg);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeWhenNotConfigured() {
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeForStarFilter() {
+    configureDefaultSubmitType("*", SubmitType.CHERRY_PICK);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.CHERRY_PICK);
+
+    configureDefaultSubmitType("*", SubmitType.FAST_FORWARD_ONLY);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.FAST_FORWARD_ONLY);
+
+    configureDefaultSubmitType("*", SubmitType.REBASE_IF_NECESSARY);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeForSpecificFilter() {
+    configureDefaultSubmitType("someProject", SubmitType.CHERRY_PICK);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someOtherProject")))
+        .isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.CHERRY_PICK);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeForStartWithFilter() {
+    configureDefaultSubmitType("somePath/somePath/*",
+        SubmitType.REBASE_IF_NECESSARY);
+    configureDefaultSubmitType("somePath/*", SubmitType.CHERRY_PICK);
+    configureDefaultSubmitType("*", SubmitType.MERGE_ALWAYS);
+
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.MERGE_ALWAYS);
+
+    assertThat(
+        repoCfg.getDefaultSubmitType(new NameKey("somePath/someProject")))
+        .isEqualTo(SubmitType.CHERRY_PICK);
+
+    assertThat(
+        repoCfg.getDefaultSubmitType(new NameKey(
+            "somePath/somePath/someProject"))).isEqualTo(
+        SubmitType.REBASE_IF_NECESSARY);
+  }
+
+  private void configureDefaultSubmitType(String projectFilter,
+      SubmitType submitType) {
+    cfg.setString(RepositoryConfig.SECTION_NAME, projectFilter,
+        RepositoryConfig.DEFAULT_SUBMIT_TYPE_NAME, submitType.toString());
+  }
+
+  @Test
+  public void testOwnerGroupsWhenNotConfigured() {
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        new String[] {});
+  }
+
+  @Test
+  public void testOwnerGroupsForStarFilter() {
+    String[] ownerGroups = new String[] {"group1", "group2"};
+    configureOwnerGroups("*", Lists.newArrayList(ownerGroups));
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        ownerGroups);
+  }
+
+  @Test
+  public void testOwnerGroupsForSpecificFilter() {
+    String[] ownerGroups = new String[] {"group1", "group2"};
+    configureOwnerGroups("someProject", Lists.newArrayList(ownerGroups));
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someOtherProject")))
+        .isEqualTo(new String[] {});
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        ownerGroups);
+  }
+
+  @Test
+  public void testOwnerGroupsForStartWithFilter() {
+    String[] ownerGroups1 = new String[] {"group1"};
+    String[] ownerGroups2 = new String[] {"group2"};
+    String[] ownerGroups3 = new String[] {"group3"};
+
+    configureOwnerGroups("*", Lists.newArrayList(ownerGroups1));
+    configureOwnerGroups("somePath/*", Lists.newArrayList(ownerGroups2));
+    configureOwnerGroups("somePath/somePath/*",
+        Lists.newArrayList(ownerGroups3));
+
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        ownerGroups1);
+
+    assertThat(repoCfg.getOwnerGroups(new NameKey("somePath/someProject")))
+        .isEqualTo(ownerGroups2);
+
+    assertThat(
+        repoCfg.getOwnerGroups(new NameKey("somePath/somePath/someProject")))
+        .isEqualTo(ownerGroups3);
+  }
+
+  private void configureOwnerGroups(String projectFilter,
+      List<String> ownerGroups) {
+    cfg.setStringList(RepositoryConfig.SECTION_NAME, projectFilter,
+        RepositoryConfig.OWNER_GROUP_NAME, ownerGroups);
+  }
+}