Allow disabling the owners auto-assigned on a per-branch basis

Define a new global configuration file <plugin-name>.config which allows
to selectively disable the resolution and auto-assignment of owners on a
per-branch basis.

Also apply the branch normalization into a generic ref using the same
logic across owners and owners-autoassign.

Change-Id: If6047069032012b6411251b06e3aff14f2a52a29
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfig.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfig.java
index 48ec686..a50ac69 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfig.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfig.java
@@ -47,4 +47,8 @@
         .projectSpecificConfig(projectKey)
         .getEnum(PROJECT_CONFIG_AUTOASSIGN_FIELD, ReviewerState.REVIEWER);
   }
+
+  public boolean isBranchDisabled(String branch) {
+    return config.isBranchDisabled(branch);
+  }
 }
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
index 70cc5a6..3f30dc2 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
@@ -209,7 +209,12 @@
       ChangeInfo change = cApi.get();
       PatchList patchList = getPatchList(repository, event, change);
       if (patchList != null) {
-        PathOwners owners = new PathOwners(accounts, repository, Optional.of(change.branch), patchList);
+        PathOwners owners =
+            new PathOwners(
+                accounts,
+                repository,
+                cfg.isBranchDisabled(change.branch) ? Optional.empty() : Optional.of(change.branch),
+                patchList);
         Set<Account.Id> allReviewers = Sets.newHashSet();
         allReviewers.addAll(owners.get().values());
         allReviewers.addAll(owners.getReviewers().values());
diff --git a/owners-autoassign/src/main/resources/Documentation/config.md b/owners-autoassign/src/main/resources/Documentation/config.md
index afb534c..fa8a30f 100644
--- a/owners-autoassign/src/main/resources/Documentation/config.md
+++ b/owners-autoassign/src/main/resources/Documentation/config.md
@@ -1,3 +1,19 @@
+## Global configuration
+
+The global plugin configuration is read from the `$GERRIT_SITE/etc/owners-autoassign.config`
+and is applied across all projects in Gerrit.
+
+owners.disable.branch
+:	List of branches regex where the resolution and auto-assignment of owners is disabled.
+
+Example:
+
+```
+[owners "disable"]
+  branch = refs/meta/config
+  branch = refs/heads/sandboxes.*
+```
+
 ## Project configuration
 
 The project configuration `autoAssignWip` controls the automatic
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/AbstractAutoassignIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/AbstractAutoassignIT.java
index 4aa6ff4..3ca6f35 100644
--- a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/AbstractAutoassignIT.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/AbstractAutoassignIT.java
@@ -19,6 +19,8 @@
 import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_FIELD;
 
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GlobalPluginConfig;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -109,6 +111,18 @@
   }
 
   @Test
+  @UseLocalDisk // Required when using @GlobalPluginConfig
+  @GlobalPluginConfig(
+      pluginName = "owners-api",
+      name = "owners.disable.branch",
+      value = "refs/heads/master")
+  public void shouldNotAutoassignUserInPathWhenBranchIsDisabled() throws Exception {
+    addOwnersToRepo("", user.email(), NOT_INHERITED);
+
+    assertThat(getAutoassignedAccounts(change(createChange()).get())).isNull();
+  }
+
+  @Test
   public void shouldNotReAutoassignUserInPath() throws Exception {
     String ownerEmail = user.email();
 
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java
index 02f98ad..2c8fc11 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java
@@ -24,6 +24,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
 
 /** Global owners plugin's settings defined globally or on a per-project basis. */
 @Singleton
@@ -53,6 +54,20 @@
   }
 
   /**
+   * Check if the branch or ref is enabled for processing.
+   *
+   * <p>NOTE: If the branch does not start with 'refs/heads' it will then normalized into a
+   * ref-name.
+   *
+   * @param branch or ref name
+   * @return true if the branch or ref is disabled for processing.
+   */
+  public boolean isBranchDisabled(String branch) {
+    String normalizedRef = normalizeRef(branch);
+    return disabledBranchesPatterns.stream().anyMatch(normalizedRef::matches);
+  }
+
+  /**
    * Project-specific config of the owners plugin.
    *
    * @param projectKey project name
@@ -63,4 +78,18 @@
       throws NoSuchProjectException {
     return configFactory.getFromProjectConfigWithInheritance(projectKey, ownersPluginName);
   }
+
+  // Logic copied from JGit's TestRepository
+  private static String normalizeRef(String ref) {
+    if (Constants.HEAD.equals(ref)) {
+      // nothing
+    } else if ("FETCH_HEAD".equals(ref)) {
+      // nothing
+    } else if ("MERGE_HEAD".equals(ref)) {
+      // nothing
+    } else if (ref.startsWith(Constants.R_REFS)) {
+      // nothing
+    } else ref = Constants.R_HEADS + ref;
+    return ref;
+  }
 }
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PluginSettingsTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PluginSettingsTest.java
new file mode 100644
index 0000000..c4c75d4
--- /dev/null
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PluginSettingsTest.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2022 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.googlesource.gerrit.owners.common;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.server.config.PluginConfigFactory;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PluginSettingsTest {
+  private static final String PLUGIN_NAME = "plugin-name";
+  @Mock PluginConfigFactory mockPluginConfigFactory;
+
+  private PluginSettings pluginSettings;
+
+  public void setupMocks(Config pluginConfig) {
+    when(mockPluginConfigFactory.getGlobalPluginConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
+    pluginSettings = new PluginSettings(mockPluginConfigFactory, PLUGIN_NAME);
+  }
+
+  @Test
+  public void allBranchesAreEnabledByDefault() {
+    setupMocks(new Config());
+
+    assertThat(pluginSettings.disabledBranchPatterns()).isEmpty();
+    assertThat(pluginSettings.isBranchDisabled("some-branch")).isFalse();
+  }
+
+  @Test
+  public void branchRefShouldBeDisabled() {
+    String branchName = "refs/heads/some-branch";
+    Config pluginConfig = new Config();
+    pluginConfig.setString("owners", "disable", "branch", branchName);
+    setupMocks(pluginConfig);
+
+    assertThat(pluginSettings.disabledBranchPatterns()).contains(branchName);
+    assertThat(pluginSettings.isBranchDisabled(branchName)).isTrue();
+  }
+
+  @Test
+  public void branchNameShouldBeDisabled() {
+    String branchName = "some-branch";
+    String branchRefName = Constants.R_HEADS + branchName;
+    Config pluginConfig = new Config();
+    pluginConfig.setString("owners", "disable", "branch", branchRefName);
+    setupMocks(pluginConfig);
+
+    assertThat(pluginSettings.disabledBranchPatterns()).contains(branchRefName);
+    assertThat(pluginSettings.isBranchDisabled(branchName)).isTrue();
+  }
+
+  @Test
+  public void branchNameShouldBeDisabledByRegex() {
+    String branchName1 = "some-branch-1";
+    String branchName2 = "some-branch-2";
+    String branchRefRegex = Constants.R_HEADS + "some-branch-\\d";
+    Config pluginConfig = new Config();
+    pluginConfig.setString("owners", "disable", "branch", branchRefRegex);
+    setupMocks(pluginConfig);
+
+    assertThat(pluginSettings.disabledBranchPatterns()).contains(branchRefRegex);
+    assertThat(pluginSettings.isBranchDisabled(branchName1)).isTrue();
+    assertThat(pluginSettings.isBranchDisabled(branchName2)).isTrue();
+  }
+}
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java
index b96ab5b..7b1399a 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java
@@ -28,7 +28,7 @@
 public class OwnerPredicateProvider implements PredicateProvider {
   @Inject
   public OwnerPredicateProvider(Accounts accounts, PluginSettings config) {
-    OwnersStoredValues.initialize(accounts, config.disabledBranchPatterns());
+    OwnersStoredValues.initialize(accounts, config);
   }
 
   @Override
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
index e749cf2..2d2267e 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
@@ -16,13 +16,13 @@
 
 package com.googlesource.gerrit.owners;
 
-import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.rules.StoredValue;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlesource.gerrit.owners.common.Accounts;
 import com.googlesource.gerrit.owners.common.PathOwners;
+import com.googlesource.gerrit.owners.common.PluginSettings;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
@@ -34,8 +34,7 @@
 
   public static StoredValue<PathOwners> PATH_OWNERS;
 
-  public static synchronized void initialize(
-      Accounts accounts, ImmutableSet<String> disablePatterns) {
+  public static synchronized void initialize(Accounts accounts, PluginSettings settings) {
     if (PATH_OWNERS != null) {
       return;
     }
@@ -47,12 +46,11 @@
             PatchList patchList = StoredValues.PATCH_LIST.get(engine);
             Repository repository = StoredValues.REPOSITORY.get(engine);
             String branch = StoredValues.getChange(engine).getDest().branch();
-            for (String pattern : disablePatterns) {
-              if (branch.matches(pattern)) {
-                return new PathOwners(accounts, repository, Optional.empty(), patchList);
-              }
-            }
-            return new PathOwners(accounts, repository, Optional.of(branch), patchList);
+            return new PathOwners(
+                accounts,
+                repository,
+                settings.isBranchDisabled(branch) ? Optional.empty() : Optional.of(branch),
+                patchList);
           }
         };
   }