Make uploadvalidator ref-aware

Add support to limit validation on specific refs.

feature: issue 5230
Change-Id: Iba99d6dd73c403857faaa9aeb7aff4d1de1bb591
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
index b6c581c..cf6f31a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
@@ -83,18 +83,21 @@
   private final GitRepositoryManager repoManager;
   private final LoadingCache<String, Pattern> patternCache;
   private final ContentTypeUtil contentTypeUtil;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   BlockedKeywordValidator(@PluginName String pluginName,
       ContentTypeUtil contentTypeUtil,
       @Named(CACHE_NAME) LoadingCache<String, Pattern> patternCache,
       PluginConfigFactory cfgFactory,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.patternCache = patternCache;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
     this.contentTypeUtil = contentTypeUtil;
+    this.validatorConfig = validatorConfig;
   }
 
   static boolean isActive(PluginConfig cfg) {
@@ -108,20 +111,20 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      ImmutableMap<String, Pattern> blockedKeywordPatterns =
-          patternCache.getAll(Arrays
-              .asList(cfg.getStringList(KEY_CHECK_BLOCKED_KEYWORD_PATTERN)));
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit,
-                blockedKeywordPatterns.values(), cfg);
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException(
-              "includes files containing blocked keywords", messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        ImmutableMap<String, Pattern> blockedKeywordPatterns =
+            patternCache.getAll(Arrays
+                .asList(cfg.getStringList(KEY_CHECK_BLOCKED_KEYWORD_PATTERN)));
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit,
+                  blockedKeywordPatterns.values(), cfg);
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException(
+                "includes files containing blocked keywords", messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException | ExecutionException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ConfigFactory.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ConfigFactory.java
new file mode 100644
index 0000000..7f80460
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ConfigFactory.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 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.plugins.uploadvalidator;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
+
+public interface ConfigFactory {
+
+  public PluginConfig get(Project.NameKey projectName);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
index ee598c3..d137252 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
@@ -92,16 +92,19 @@
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
   private final ContentTypeUtil contentTypeUtil;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   ContentTypeValidator(@PluginName String pluginName,
       ContentTypeUtil contentTypeUtil,
       PluginConfigFactory cfgFactory,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.contentTypeUtil = contentTypeUtil;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
   }
 
   @Override
@@ -111,17 +114,17 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit, getBlockedTypes(cfg),
-                isWhitelist(cfg));
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException("contains blocked content type",
-              messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit, getBlockedTypes(cfg),
+                  isWhitelist(cfg));
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException("contains blocked content type",
+                messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException | ExecutionException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
index 75c18d2..3a44758 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
@@ -145,6 +145,7 @@
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
+  private final ValidatorConfig validatorConfig;
 
   private Locale locale;
 
@@ -155,10 +156,12 @@
 
   @Inject
   DuplicatePathnameValidator(@PluginName String pluginName,
-      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager) {
+      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
   }
 
   @Override
@@ -168,17 +171,17 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      locale = getLocale(cfg);
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit);
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException("contains duplicate pathnames",
-              messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        locale = getLocale(cfg);
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit);
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException("contains duplicate pathnames",
+                messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
index 3f455bd..3e61ee6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
@@ -64,13 +64,16 @@
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   FileExtensionValidator(@PluginName String pluginName,
-      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager) {
+      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
   }
 
   private static List<String> getBlockedExtensions(PluginConfig cfg) {
@@ -92,16 +95,16 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages = performValidation(repo,
-            receiveEvent.commit, getBlockedExtensions(cfg));
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException(
-              "contains files with blocked file extensions", messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages = performValidation(repo,
+              receiveEvent.commit, getBlockedExtensions(cfg));
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException(
+                "contains files with blocked file extensions", messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
index 420e47a..3c466d8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
@@ -62,11 +62,14 @@
 
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
-  FooterValidator(@PluginName String pluginName, PluginConfigFactory cfgFactory) {
+  FooterValidator(@PluginName String pluginName, PluginConfigFactory cfgFactory,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
+    this.validatorConfig = validatorConfig;
   }
 
   @Override
@@ -78,7 +81,8 @@
               receiveEvent.project.getNameKey(), pluginName);
       String[] requiredFooters =
           cfg.getStringList(KEY_REQUIRED_FOOTER);
-      if (requiredFooters.length > 0) {
+      if (requiredFooters.length > 0 && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
         List<CommitValidationMessage> messages = new LinkedList<>();
         Set<String> footers = FluentIterable.from(receiveEvent.commit.getFooterLines())
             .transform(new Function<FooterLine, String>() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
index 973be15..d912f5c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
@@ -64,13 +64,16 @@
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   InvalidFilenameValidator(@PluginName String pluginName,
-      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager) {
+      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
   }
 
   static boolean isActive(PluginConfig cfg) {
@@ -84,17 +87,17 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      try (Repository repo = repoManager.openRepository(
-          receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit,
-                cfg.getStringList(KEY_INVALID_FILENAME_PATTERN));
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException(
-              "contains files with an invalid filename", messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        try (Repository repo = repoManager.openRepository(
+            receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit,
+                  cfg.getStringList(KEY_INVALID_FILENAME_PATTERN));
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException(
+                "contains files with an invalid filename", messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
index 7183b69..8e50232 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
@@ -73,16 +73,19 @@
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
   private final ContentTypeUtil contentTypeUtil;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   InvalidLineEndingValidator(@PluginName String pluginName,
       ContentTypeUtil contentTypeUtil,
       PluginConfigFactory cfgFactory,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
     this.contentTypeUtil = contentTypeUtil;
+    this.validatorConfig = validatorConfig;
   }
 
   static boolean isActive(PluginConfig cfg) {
@@ -96,16 +99,16 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit, cfg);
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException(
-              "contains files with a Windows line ending", messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit, cfg);
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException(
+                "contains files with a Windows line ending", messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException | ExecutionException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
index aa898dd..f13d16b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
@@ -61,13 +61,16 @@
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   MaxPathLengthValidator(@PluginName String pluginName,
-      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager) {
+      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
   }
 
   static boolean isActive(PluginConfig cfg) {
@@ -81,18 +84,18 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      int maxPathLength = cfg.getInt(KEY_MAX_PATH_LENGTH, 0);
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit, maxPathLength);
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException(
-              "contains files with too long paths (max path length: "
-                  + maxPathLength + ")", messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        int maxPathLength = cfg.getInt(KEY_MAX_PATH_LENGTH, 0);
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit, maxPathLength);
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException(
+                "contains files with too long paths (max path length: "
+                    + maxPathLength + ")", messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
index a3cbfc4..b1cba3c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.uploadvalidator;
 
 import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
 
 class Module extends AbstractModule {
 
@@ -33,5 +34,8 @@
     install(InvalidLineEndingValidator.module());
     install(ContentTypeValidator.module());
     install(DuplicatePathnameValidator.module());
+
+    bind(ConfigFactory.class).to(PluginConfigWithInheritanceFactory.class).in(
+        Scopes.SINGLETON);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigWithInheritanceFactory.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigWithInheritanceFactory.java
new file mode 100644
index 0000000..1e90f2c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigWithInheritanceFactory.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2017 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.plugins.uploadvalidator;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PluginConfigWithInheritanceFactory implements ConfigFactory {
+  private static final Logger log = LoggerFactory.getLogger(PluginConfigWithInheritanceFactory.class);
+  private final PluginConfigFactory pluginConfigFactory;
+  private final String pluginName;
+
+  @Inject
+  public PluginConfigWithInheritanceFactory(PluginConfigFactory pcf,
+      @PluginName String pn) {
+    this.pluginConfigFactory = pcf;
+    this.pluginName = pn;
+  }
+
+  @Override
+  public PluginConfig get(Project.NameKey projectName) {
+    try {
+      return pluginConfigFactory.getFromProjectConfigWithInheritance(projectName,
+          pluginName);
+    } catch (NoSuchProjectException e) {
+      log.warn(projectName.get() + " not found");
+      return null;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
index b58418d..e1a492f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
@@ -63,13 +63,16 @@
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   SubmoduleValidator(@PluginName String pluginName,
-      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager) {
+      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
   }
 
   static boolean isActive(PluginConfig cfg) {
@@ -83,15 +86,15 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit);
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException("contains submodules", messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit);
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException("contains submodules", messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
index b5a090d..f3c4123 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
@@ -64,13 +64,16 @@
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
+  private final ValidatorConfig validatorConfig;
 
   @Inject
   SymlinkValidator(@PluginName String pluginName,
-      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager) {
+      PluginConfigFactory cfgFactory, GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
   }
 
   static boolean isActive(PluginConfig cfg) {
@@ -84,16 +87,16 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (!isActive(cfg)) {
-        return Collections.emptyList();
-      }
-      try (Repository repo =
-          repoManager.openRepository(receiveEvent.project.getNameKey())) {
-        List<CommitValidationMessage> messages =
-            performValidation(repo, receiveEvent.commit);
-        if (!messages.isEmpty()) {
-          throw new CommitValidationException("contains symbolic links",
-              messages);
+      if (isActive(cfg) && validatorConfig.isEnabledForRef(
+          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+        try (Repository repo =
+            repoManager.openRepository(receiveEvent.project.getNameKey())) {
+          List<CommitValidationMessage> messages =
+              performValidation(repo, receiveEvent.commit);
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException("contains symbolic links",
+                messages);
+          }
         }
       }
     } catch (NoSuchProjectException | IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
new file mode 100644
index 0000000..3a48288
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2017 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.plugins.uploadvalidator;
+
+import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.project.RefPatternMatcher;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ValidatorConfig {
+  private static final Logger log = LoggerFactory
+      .getLogger(ValidatorConfig.class);
+
+  private final ConfigFactory configFactory;
+
+  @Inject
+  public ValidatorConfig(ConfigFactory configFactory) {
+    this.configFactory = configFactory;
+  }
+
+  public boolean isEnabledForRef(Project.NameKey projectName, String refName) {
+    PluginConfig pluginConfig = configFactory.get(projectName);
+    if (pluginConfig == null) {
+      log.error("Failed to check if validation is enabled for project "
+          + projectName.get() + ": Plugin config not found");
+      return false;
+    }
+    if(!isValidConfig(pluginConfig, projectName)) {
+      return false;
+    }
+
+    String[] refPatterns = pluginConfig.getStringList("ref");
+    if (refPatterns.length == 0) {
+      return true; // Default behavior: no branch-specific config
+    }
+
+    for (String refPattern : refPatterns) {
+      if (match(refName, refPattern)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean isValidConfig(PluginConfig config, Project.NameKey projectName) {
+    boolean valid = true;
+    for (String refPattern : config.getStringList("ref")) {
+      if (!RefConfigSection.isValid(refPattern)) {
+        log.error(
+            "Invalid ref name/pattern/regex '{}' in {} project's plugin config",
+            refPattern, projectName.get());
+        valid = false;
+      }
+    }
+
+    return valid;
+  }
+
+  private static boolean match(String refName, String refPattern) {
+    return RefPatternMatcher.getMatcher(refPattern).match(refName, null);
+  }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 2fafca7..f958126 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -206,3 +206,24 @@
 [Project's inheritance rules][7].
 
 [7]: config-project-config.html#file-project_config
+
+Ref-specific validations
+---------------------------
+
+By default, the validation will be enabled for all refs. However, it can
+be limited to particular refs by setting `plugin.@PLUGIN@.ref`. The
+refs may be configured using specific ref names, ref patterns, or regular
+expressions. Multiple refs may be specified.
+
+NOTE: ref needs to start with "ref/" for name and pattern or "^ref/" for regex
+matching. When an invalid ref is specified in the Project config, the plugin
+functionality is disabled, and an error is reported in the Gerrit log.
+
+E.g. to limit the validation to the `master` branch and all stable
+branches the following could be configured:
+
+```
+  [plugin "@PLUGIN@"]
+    ref = refs/heads/master
+    ref = ^refs/heads/stable-.*
+```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidatorTest.java
index 5f066ed..0a2722f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidatorTest.java
@@ -76,7 +76,7 @@
   public void testKeywords() throws Exception {
     RevCommit c = makeCommit();
     BlockedKeywordValidator validator = new BlockedKeywordValidator(null,
-        new ContentTypeUtil(PATTERN_CACHE), PATTERN_CACHE, null, null);
+        new ContentTypeUtil(PATTERN_CACHE), PATTERN_CACHE, null, null, null);
     List<CommitValidationMessage> m = validator.performValidation(
         repo, c, getPatterns().values(), EMPTY_PLUGIN_CONFIG);
     Set<String> expected = ImmutableSet.of(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidatorTest.java
index 6d2c373..7a509ce 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidatorTest.java
@@ -58,7 +58,7 @@
   @Before
   public void setUp() {
     validator = new ContentTypeValidator(
-        null, new ContentTypeUtil(PATTERN_CACHE), null, null);
+        null, new ContentTypeUtil(PATTERN_CACHE), null, null, null);
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidatorTest.java
index f3be61b..5ebb53f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidatorTest.java
@@ -72,7 +72,7 @@
   public void init() throws IOException {
     super.init();
     testRepo = new TestRepository<>(repo);
-    validator = new DuplicatePathnameValidator(null, null, null);
+    validator = new DuplicatePathnameValidator(null, null, null, null);
     validator.setLocale(Locale.ENGLISH);
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeConfigFactory.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeConfigFactory.java
new file mode 100644
index 0000000..181810e
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeConfigFactory.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2017 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.plugins.uploadvalidator;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.config.PluginConfig;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+public class FakeConfigFactory implements ConfigFactory {
+  private final Config config;
+  private final Project.NameKey projectName;
+
+  public FakeConfigFactory(Project.NameKey projectName, String configText)
+      throws ConfigInvalidException {
+    this.config = new Config();
+    this.config.fromText(configText);
+    this.projectName = projectName;
+  }
+
+  @Override
+  public PluginConfig get(NameKey projectName) {
+    if (this.projectName.equals(projectName)) {
+      return new PluginConfig("uploadvalidator", config);
+    }
+
+    return new PluginConfig("uploadvalidator", new Config());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidatorTest.java
index 3ea1b1b..50981be 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidatorTest.java
@@ -59,7 +59,7 @@
   public void testCarriageReturn() throws Exception {
     RevCommit c = makeCommit();
     InvalidLineEndingValidator validator = new InvalidLineEndingValidator(null,
-        new ContentTypeUtil(PATTERN_CACHE), null, null);
+        new ContentTypeUtil(PATTERN_CACHE), null, null, null);
     List<CommitValidationMessage> m = validator.performValidation(repo, c,
         EMPTY_PLUGIN_CONFIG);
     assertThat(TestUtils.transformMessages(m))
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java
new file mode 100644
index 0000000..962963b
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2017 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.plugins.uploadvalidator;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+import static com.google.common.truth.Truth.*;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.junit.Test;
+
+public class RefAwareValidatorConfigTest {
+  private Project.NameKey projectName = new Project.NameKey("testProject");
+
+  @Test
+  public void isEnabledForAllRefsByDefault() throws Exception {
+    ValidatorConfig config =
+        getConfig("[plugin \"uploadvalidator\"]\n"
+            + "blockedFileExtension = jar");
+
+    assertThat(config.isEnabledForRef(projectName, "anyRef")).isTrue();
+  }
+
+  @Test
+  public void isEnabledForSingleRef() throws Exception {
+    ValidatorConfig config =
+        getConfig("[plugin \"uploadvalidator\"]\n"
+            + "   ref = refs/heads/anyref\n"
+            + "   blockedFileExtension = jar");
+
+    assertThat(config.isEnabledForRef(projectName, "refs/heads/anyref"))
+        .isTrue();
+  }
+
+  @Test
+  public void isDisabledForInvalidRef() throws Exception {
+    ValidatorConfig config =
+        getConfig("[plugin \"uploadvalidator\"]\n"
+            + "   ref = anInvalidRef\n"
+            + "   blockedFileExtension = jar");
+
+    assertThat(config.isEnabledForRef(projectName, "refs/heads/anyref"))
+        .isFalse();
+  }
+
+  @Test
+  public void isEnabledForRegexRef() throws Exception {
+    ValidatorConfig config =
+        getConfig("[plugin \"uploadvalidator\"]\n"
+            + "   ref = ^refs/heads/mybranch.*\n"
+            + "   blockedFileExtension = jar");
+
+    assertThat(config.isEnabledForRef(projectName, "refs/heads/anotherref"))
+        .isFalse();
+    assertThat(config.isEnabledForRef(projectName, "refs/heads/mybranch123"))
+        .isTrue();
+  }
+
+  @Test
+  public void isEnabledForMultipleRefs() throws Exception {
+    ValidatorConfig config =
+        getConfig("[plugin \"uploadvalidator\"]\n"
+            + "   ref = refs/heads/branch1\n"
+            + "   ref = refs/heads/branch2\n"
+            + "   blockedFileExtension = jar");
+
+    assertThat(config.isEnabledForRef(projectName, "refs/heads/branch1"))
+        .isTrue();
+    assertThat(config.isEnabledForRef(projectName, "refs/heads/branch2"))
+        .isTrue();
+    assertThat(config.isEnabledForRef(projectName, "refs/heads/branch3"))
+        .isFalse();
+  }
+
+  private ValidatorConfig getConfig(String defaultConfig)
+      throws ConfigInvalidException {
+    ValidatorConfig config =
+        new ValidatorConfig(new FakeConfigFactory(projectName, defaultConfig));
+    return config;
+  }
+}