Allow filter events broadcasting by project name

Add possibility to configure which projects should broadcast events
via events broker. Some projects are local and should not be shared
with anyone else outside the site. It doesn't make sense to replicate
them and broadcast their events across the globe.

Feature: Issue 13262
Change-Id: I0ea1c7f68a5c19035b7e00cf40e468baba9ef381
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
index ebe28d4..f509cd4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
@@ -61,6 +61,7 @@
   private final Supplier<Cache> cache;
   private final Supplier<Event> event;
   private final Supplier<Index> index;
+  private final Supplier<Projects> projects;
   private final Supplier<SharedRefDatabase> sharedRefDb;
   private final Supplier<Collection<Message>> replicationConfigValidation;
   private final Supplier<Broker> broker;
@@ -79,6 +80,7 @@
     cache = memoize(() -> new Cache(lazyMultiSiteCfg));
     event = memoize(() -> new Event(lazyMultiSiteCfg));
     index = memoize(() -> new Index(lazyMultiSiteCfg));
+    projects = memoize(() -> new Projects(lazyMultiSiteCfg));
     sharedRefDb = memoize(() -> new SharedRefDatabase(lazyMultiSiteCfg));
     broker = memoize(() -> new Broker(lazyMultiSiteCfg));
   }
@@ -107,6 +109,10 @@
     return broker.get();
   }
 
+  public Projects projects() {
+    return projects.get();
+  }
+
   public Collection<Message> validate() {
     return replicationConfigValidation.get();
   }
@@ -198,6 +204,20 @@
     }
   }
 
+  public static class Projects {
+    public static final String SECTION = "projects";
+    public static final String PATTERN_KEY = "pattern";
+    public List<String> patterns;
+
+    public Projects(Supplier<Config> cfg) {
+      patterns = ImmutableList.copyOf(cfg.get().getStringList("projects", null, PATTERN_KEY));
+    }
+
+    public List<String> getPatterns() {
+      return patterns;
+    }
+  }
+
   /** Common parameters to cache, event, index */
   public abstract static class Forwarding {
     static final boolean DEFAULT_SYNCHRONIZE = true;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/ProjectsFilter.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/ProjectsFilter.java
new file mode 100644
index 0000000..d6fac47
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/ProjectsFilter.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2020 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.multisite;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.ProjectEvent;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.List;
+import java.util.Set;
+
+@Singleton
+public class ProjectsFilter {
+  public enum PatternType {
+    REGEX,
+    WILDCARD,
+    EXACT_MATCH;
+
+    public static PatternType getPatternType(String pattern) {
+      if (pattern.startsWith(AccessSection.REGEX_PREFIX)) {
+        return REGEX;
+      } else if (pattern.endsWith("*")) {
+        return WILDCARD;
+      } else {
+        return EXACT_MATCH;
+      }
+    }
+  }
+
+  private Set<NameKey> globalProjects = Sets.newConcurrentHashSet();
+  private Set<NameKey> localProjects = Sets.newConcurrentHashSet();
+  private final List<String> projectPatterns;
+
+  @Inject
+  public ProjectsFilter(Configuration cfg) {
+    projectPatterns = cfg.projects().getPatterns();
+  }
+
+  public boolean matches(Event event) {
+    if (event == null) {
+      throw new IllegalArgumentException("Event object cannot be null");
+    }
+    if (event instanceof ProjectEvent) {
+      return matches(((ProjectEvent) event).getProjectNameKey());
+    }
+    return false;
+  }
+
+  public boolean matches(String projectName) {
+    return matches(NameKey.parse(projectName));
+  }
+
+  public boolean matches(Project.NameKey name) {
+    if (name == null || Strings.isNullOrEmpty(name.get())) {
+      throw new IllegalArgumentException(
+          String.format("Project name cannot be null or empty, but was %s", name));
+    }
+    if (projectPatterns.isEmpty() || globalProjects.contains(name)) {
+      return true;
+    }
+
+    if (localProjects.contains(name)) {
+      return false;
+    }
+
+    String projectName = name.get();
+
+    for (String pattern : projectPatterns) {
+      if (matchesPattern(projectName, pattern)) {
+        globalProjects.add(name);
+        return true;
+      }
+    }
+    localProjects.add(name);
+    return false;
+  }
+
+  private boolean matchesPattern(String projectName, String pattern) {
+    boolean match = false;
+    switch (PatternType.getPatternType(pattern)) {
+      case REGEX:
+        match = projectName.matches(pattern);
+        break;
+      case WILDCARD:
+        match = projectName.startsWith(pattern.substring(0, pattern.length() - 1));
+        break;
+      case EXACT_MATCH:
+        match = projectName.equals(pattern);
+    }
+    return match;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java
index 1060977..2e2f9de 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ForwarderTask;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ProjectListUpdateForwarder;
@@ -31,12 +32,16 @@
 
   private final DynamicSet<ProjectListUpdateForwarder> forwarders;
   private final Executor executor;
+  private final ProjectsFilter projectsFilter;
 
   @Inject
   public ProjectListUpdateHandler(
-      DynamicSet<ProjectListUpdateForwarder> forwarders, @CacheExecutor Executor executor) {
+      DynamicSet<ProjectListUpdateForwarder> forwarders,
+      @CacheExecutor Executor executor,
+      ProjectsFilter filter) {
     this.forwarders = forwarders;
     this.executor = executor;
+    this.projectsFilter = filter;
   }
 
   @Override
@@ -52,7 +57,7 @@
   }
 
   private void process(ProjectEvent event, boolean delete) {
-    if (!Context.isForwardedEvent()) {
+    if (!Context.isForwardedEvent() && projectsFilter.matches(event.getProjectName())) {
       executor.execute(
           new ProjectListUpdateTask(new ProjectListUpdateEvent(event.getProjectName(), delete)));
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java
index aafd59f..a1b7a53 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.events.EventListener;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.forwarder.StreamEventForwarder;
 import java.util.concurrent.Executor;
@@ -26,17 +27,24 @@
 class EventHandler implements EventListener {
   private final Executor executor;
   private final DynamicSet<StreamEventForwarder> forwarders;
+  private final ProjectsFilter projectsFilter;
 
   @Inject
-  EventHandler(DynamicSet<StreamEventForwarder> forwarders, @EventExecutor Executor executor) {
+  EventHandler(
+      DynamicSet<StreamEventForwarder> forwarders,
+      @EventExecutor Executor executor,
+      ProjectsFilter projectsFilter) {
     this.forwarders = forwarders;
     this.executor = executor;
+    this.projectsFilter = projectsFilter;
   }
 
   @Override
   public void onEvent(Event event) {
     if (!Context.isForwardedEvent() && event instanceof ProjectEvent) {
-      executor.execute(new EventTask(event));
+      if (projectsFilter.matches(event)) {
+        executor.execute(new EventTask(event));
+      }
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
index 62359cb..fe6bdac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ForwarderTask;
 import com.googlesource.gerrit.plugins.multisite.forwarder.IndexEventForwarder;
@@ -45,15 +46,18 @@
   private final DynamicSet<IndexEventForwarder> forwarders;
   private final Set<IndexTask> queuedTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
   private final ChangeCheckerImpl.Factory changeChecker;
+  private final ProjectsFilter projectsFilter;
 
   @Inject
   IndexEventHandler(
       @IndexExecutor Executor executor,
       DynamicSet<IndexEventForwarder> forwarders,
-      ChangeCheckerImpl.Factory changeChecker) {
+      ChangeCheckerImpl.Factory changeChecker,
+      ProjectsFilter projectsFilter) {
     this.forwarders = forwarders;
     this.executor = executor;
     this.changeChecker = changeChecker;
+    this.projectsFilter = projectsFilter;
   }
 
   @Override
@@ -88,7 +92,7 @@
 
   @Override
   public void onProjectIndexed(String projectName) {
-    if (!Context.isForwardedEvent()) {
+    if (!Context.isForwardedEvent() && projectsFilter.matches(projectName)) {
       IndexProjectTask task = new IndexProjectTask(new ProjectIndexEvent(projectName));
       if (queuedTasks.add(task)) {
         executor.execute(task);
@@ -97,7 +101,7 @@
   }
 
   private void executeIndexChangeTask(String projectName, int id) {
-    if (!Context.isForwardedEvent()) {
+    if (!Context.isForwardedEvent() && projectsFilter.matches(projectName)) {
       ChangeChecker checker = changeChecker.create(projectName + "~" + id);
 
       try {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java
index cfe585d..ebc17b7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java
@@ -18,6 +18,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.multisite.LockWrapper;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.OutOfSyncException;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement;
@@ -51,15 +52,24 @@
       ValidationMetrics validationMetrics,
       SharedRefEnforcement refEnforcement,
       LockWrapper.Factory lockWrapperFactory,
+      ProjectsFilter projectsFilter,
       @Assisted String projectName,
       @Assisted RefDatabase refDb) {
-    super(sharedRefDb, validationMetrics, refEnforcement, lockWrapperFactory, projectName, refDb);
+    super(
+        sharedRefDb,
+        validationMetrics,
+        refEnforcement,
+        lockWrapperFactory,
+        projectsFilter,
+        projectName,
+        refDb);
   }
 
   public void executeBatchUpdateWithValidation(
       BatchRefUpdate batchRefUpdate, NoParameterVoidFunction batchRefUpdateFunction)
       throws IOException {
-    if (refEnforcement.getPolicy(projectName) == EnforcePolicy.IGNORED) {
+    if (refEnforcement.getPolicy(projectName) == EnforcePolicy.IGNORED
+        || !isGlobalProject(projectName)) {
       batchRefUpdateFunction.invoke();
       return;
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
index 73bab49..c8ddb43 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
@@ -31,6 +31,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import java.io.IOException;
@@ -58,6 +59,7 @@
   private final GitRepositoryManager gitRepositoryManager;
   private final GitReferenceUpdated gitReferenceUpdated;
   private final ProjectVersionLogger verLogger;
+  private final ProjectsFilter projectsFilter;
 
   protected final SharedRefDatabaseWrapper sharedRefDb;
 
@@ -66,11 +68,13 @@
       GitRepositoryManager gitRepositoryManager,
       SharedRefDatabaseWrapper sharedRefDb,
       GitReferenceUpdated gitReferenceUpdated,
-      ProjectVersionLogger verLogger) {
+      ProjectVersionLogger verLogger,
+      ProjectsFilter projectsFilter) {
     this.gitRepositoryManager = gitRepositoryManager;
     this.sharedRefDb = sharedRefDb;
     this.gitReferenceUpdated = gitReferenceUpdated;
     this.verLogger = verLogger;
+    this.projectsFilter = projectsFilter;
   }
 
   @Override
@@ -78,7 +82,9 @@
     logger.atFine().log("Processing event type: " + event.type);
     // Producer of the Event use RefUpdatedEvent to trigger the version update
     if (!Context.isForwardedEvent() && event instanceof RefUpdatedEvent) {
-      updateProducerProjectVersionUpdate((RefUpdatedEvent) event);
+      if (projectsFilter.matches(event)) {
+        updateProducerProjectVersionUpdate((RefUpdatedEvent) event);
+      }
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java
index 89c6792..a882e2d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java
@@ -21,6 +21,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.multisite.LockWrapper;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.OutOfSyncException;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedDbSplitBrainException;
@@ -45,6 +46,7 @@
   private final LockWrapper.Factory lockWrapperFactory;
   protected final RefDatabase refDb;
   protected final SharedRefEnforcement refEnforcement;
+  protected final ProjectsFilter projectsFilter;
 
   public static interface Factory {
     RefUpdateValidator create(String projectName, RefDatabase refDb);
@@ -73,6 +75,7 @@
       ValidationMetrics validationMetrics,
       SharedRefEnforcement refEnforcement,
       LockWrapper.Factory lockWrapperFactory,
+      ProjectsFilter projectsFilter,
       @Assisted String projectName,
       @Assisted RefDatabase refDb) {
     this.sharedRefDb = sharedRefDb;
@@ -81,12 +84,14 @@
     this.refDb = refDb;
     this.projectName = projectName;
     this.refEnforcement = refEnforcement;
+    this.projectsFilter = projectsFilter;
   }
 
   public RefUpdate.Result executeRefUpdate(
       RefUpdate refUpdate, NoParameterFunction<RefUpdate.Result> refUpdateFunction)
       throws IOException {
     if (isProjectVersionUpdate(refUpdate.getName())
+        || !isGlobalProject(projectName)
         || refEnforcement.getPolicy(projectName) == EnforcePolicy.IGNORED) {
       return refUpdateFunction.invoke();
     }
@@ -124,6 +129,12 @@
     }
   }
 
+  protected Boolean isGlobalProject(String projectName) {
+    Boolean isGlobalProject = projectsFilter.matches(projectName);
+    logger.atFine().log("Is global project? " + isGlobalProject);
+    return isGlobalProject;
+  }
+
   protected RefUpdate.Result doExecuteRefUpdate(
       RefUpdate refUpdate, NoParameterFunction<RefUpdate.Result> refUpdateFunction)
       throws IOException {
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 662f46e..b1f3369 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -100,4 +100,28 @@
 
     Relax the alignment with the shared ref-database for AProject on refs/heads/feature.
 
-    Defaults: No rules = All projects are REQUIRED to be consistent on all refs.
\ No newline at end of file
+    Defaults: No rules = All projects are REQUIRED to be consistent on all refs.
+
+```projects.pattern```
+:   Specifies which projects events should be send via broker. It can be provided more
+    than once, and supports three formats: regular expressions, wildcard matching, and single
+    project matching. All three formats match case-sensitive.
+
+    Values starting with a caret `^` are treated as regular
+    expressions. For the regular expressions details please follow
+    official [java documentation](https://docs.oracle.com/javase/tutorial/essential/regex/).
+
+    Please note that regular expressions could also be used
+    with inverse match.
+
+    Values that are not regular expressions and end in `*` are
+    treated as wildcard matches. Wildcards match projects whose
+    name agrees from the beginning until the trailing `*`. So
+    `foo/b*` would match the projects `foo/b`, `foo/bar`, and
+    `foo/baz`, but neither `foobar`, nor `bar/foo/baz`.
+
+    Values that are neither regular expressions nor wildcards are
+    treated as single project matches. So `foo/bar` matches only
+    the project `foo/bar`, but no other project.
+
+    By default, all projects are matched.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/ProjectsFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/ProjectsFilterTest.java
new file mode 100644
index 0000000..e81c906
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/ProjectsFilterTest.java
@@ -0,0 +1,180 @@
+// Copyright (C) 2020 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.multisite;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.ProjectEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.testing.GerritJUnit;
+import com.googlesource.gerrit.plugins.multisite.Configuration.Projects;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ProjectsFilterTest {
+
+  @Mock private Configuration configuration;
+  @Mock private Projects projects;
+
+  private ProjectsFilter objectUnderTest;
+
+  @Test
+  public void shouldMatchByExactProjectName() {
+    when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isFalse();
+  }
+
+  @Test
+  public void shouldMatchByWildcard() {
+    when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project*"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("2test_project"))).isFalse();
+  }
+
+  @Test
+  public void shouldMatchByRegex() {
+    when(projects.getPatterns()).thenReturn(Lists.newArrayList("^test_(project|project2)"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project3"))).isFalse();
+  }
+
+  @Test
+  public void shouldExcludeByRegex() {
+    when(projects.getPatterns()).thenReturn(Lists.newArrayList("^(?:(?!test_project3).)*$"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project3"))).isFalse();
+  }
+
+  @Test
+  public void shouldExcludeByMultipleProjectsRegexPattern() {
+    when(projects.getPatterns())
+        .thenReturn(Lists.newArrayList("^(?:(?!(test_project3|test_project4)).)*$"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project3"))).isFalse();
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project4"))).isFalse();
+  }
+
+  @Test
+  public void shouldMatchWhenNoPatternProvided() {
+    when(projects.getPatterns()).thenReturn(Collections.emptyList());
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue();
+  }
+
+  @Test
+  public void shouldMatchProjectEvent() {
+    ProjectEvent event = mock(ProjectEvent.class);
+    when(event.getProjectNameKey()).thenReturn(NameKey.parse("test_project"));
+    when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(event)).isTrue();
+  }
+
+  @Test
+  public void shouldMatchRefUpdatedEvent() {
+    RefUpdatedEvent event = mock(RefUpdatedEvent.class);
+    when(event.getProjectNameKey()).thenReturn(NameKey.parse("test_project"));
+    when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(event)).isTrue();
+  }
+
+  @Test
+  public void shouldExcludedNonProjectEvents() {
+    Event event = mock(Event.class);
+    when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project)"));
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    assertThat(objectUnderTest.matches(event)).isFalse();
+  }
+
+  @Test
+  public void shouldThrowExceptionWhenProjecNameIsNull() {
+    when(projects.getPatterns()).thenReturn(Collections.emptyList());
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    GerritJUnit.assertThrows(
+        IllegalArgumentException.class, () -> objectUnderTest.matches((NameKey) null));
+  }
+
+  @Test
+  public void shouldThrowExceptionWhenProjecNameIsEmpty() {
+    when(projects.getPatterns()).thenReturn(Collections.emptyList());
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    GerritJUnit.assertThrows(
+        IllegalArgumentException.class, () -> objectUnderTest.matches(NameKey.parse("")));
+  }
+
+  @Test
+  public void shouldThrowExceptionWhenEventIsNull() {
+    when(projects.getPatterns()).thenReturn(Collections.emptyList());
+    when(configuration.projects()).thenReturn(projects);
+
+    objectUnderTest = new ProjectsFilter(configuration);
+
+    GerritJUnit.assertThrows(
+        IllegalArgumentException.class, () -> objectUnderTest.matches((Event) null));
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java
index b86f8af..2a25c37 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java
@@ -18,6 +18,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -26,6 +27,7 @@
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.cache.ProjectListUpdateHandler.ProjectListUpdateTask;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ProjectListUpdateForwarder;
@@ -42,10 +44,14 @@
   private ProjectListUpdateHandler handler;
 
   @Mock private ProjectListUpdateForwarder forwarder;
+  @Mock private ProjectsFilter projectsFilter;
 
   @Before
   public void setUp() {
-    handler = new ProjectListUpdateHandler(asDynamicSet(forwarder), MoreExecutors.directExecutor());
+    when(projectsFilter.matches(any(String.class))).thenReturn(true);
+    handler =
+        new ProjectListUpdateHandler(
+            asDynamicSet(forwarder), MoreExecutors.directExecutor(), projectsFilter);
   }
 
   private DynamicSet<ProjectListUpdateForwarder> asDynamicSet(
@@ -87,6 +93,18 @@
   }
 
   @Test
+  public void shouldNotForwardIfFilteredOutByProjectName() throws Exception {
+    when(projectsFilter.matches(any(String.class))).thenReturn(false);
+    String projectName = "projectToAdd";
+    NewProjectCreatedListener.Event event = mock(NewProjectCreatedListener.Event.class);
+    when(event.getProjectName()).thenReturn(projectName);
+    handler.onNewProjectCreated(event);
+    verify(forwarder, never())
+        .updateProjectList(
+            any(ProjectListUpdateTask.class), eq(new ProjectListUpdateEvent(projectName, true)));
+  }
+
+  @Test
   public void testProjectUpdateTaskToString() throws Exception {
     String projectName = "someProjectName";
     ProjectListUpdateTask task =
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java
index 990a03f..bac0851 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java
@@ -15,15 +15,19 @@
 package com.googlesource.gerrit.plugins.multisite.event;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.event.EventHandler.EventTask;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.forwarder.StreamEventForwarder;
@@ -39,10 +43,13 @@
   private EventHandler eventHandler;
 
   @Mock private StreamEventForwarder forwarder;
+  @Mock private ProjectsFilter projectsFilter;
 
   @Before
   public void setUp() {
-    eventHandler = new EventHandler(asDynamicSet(forwarder), MoreExecutors.directExecutor());
+    when(projectsFilter.matches(any(ProjectEvent.class))).thenReturn(true);
+    eventHandler =
+        new EventHandler(asDynamicSet(forwarder), MoreExecutors.directExecutor(), projectsFilter);
   }
 
   private DynamicSet<StreamEventForwarder> asDynamicSet(StreamEventForwarder forwarder) {
@@ -53,7 +60,7 @@
 
   @Test
   public void shouldForwardAnyProjectEvent() throws Exception {
-    Event event = mock(ProjectEvent.class);
+    ProjectEvent event = mock(ProjectEvent.class);
     eventHandler.onEvent(event);
     verify(forwarder).send(event);
   }
@@ -73,6 +80,16 @@
   }
 
   @Test
+  public void shouldNotForwardIfFilteredOutByProjectName() throws Exception {
+    when(projectsFilter.matches(any(ProjectEvent.class))).thenReturn(false);
+
+    ProjectEvent event = mock(ProjectEvent.class);
+
+    eventHandler.onEvent(event);
+    verify(forwarder, never()).send(event);
+  }
+
+  @Test
   public void tesEventTaskToString() throws Exception {
     Event event = new RefUpdatedEvent();
     EventTask task = eventHandler.new EventTask(event);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java
new file mode 100644
index 0000000..0a57851
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2020 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.multisite.index;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
+import com.googlesource.gerrit.plugins.multisite.forwarder.IndexEventForwarder;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.ProjectIndexEvent;
+import com.googlesource.gerrit.plugins.multisite.index.IndexEventHandler.IndexProjectTask;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IndexEventHandlerTest {
+
+  private IndexEventHandler eventHandler;
+
+  @Mock private ProjectsFilter projectsFilter;
+  @Mock private IndexEventForwarder forwarder;
+  @Mock private ChangeCheckerImpl.Factory changeChecker;
+
+  @Before
+  public void setUp() {
+    eventHandler =
+        new IndexEventHandler(
+            MoreExecutors.directExecutor(), asDynamicSet(forwarder), changeChecker, projectsFilter);
+  }
+
+  private DynamicSet<IndexEventForwarder> asDynamicSet(IndexEventForwarder forwarder) {
+    DynamicSet<IndexEventForwarder> result = new DynamicSet<>();
+    result.add("multi-site", forwarder);
+    return result;
+  }
+
+  @Test
+  public void shouldNotForwardProjectIndexedIfFilteredOutByProjectName() throws Exception {
+    when(projectsFilter.matches(any(String.class))).thenReturn(false);
+
+    eventHandler.onProjectIndexed("test_project");
+    verify(forwarder, never())
+        .index(any(IndexProjectTask.class), eq(new ProjectIndexEvent("test_project")));
+  }
+
+  @Test
+  public void shouldNotForwardIndexChangeIfFilteredOutByProjectName() throws Exception {
+    int changeId = 1;
+    when(projectsFilter.matches(any(String.class))).thenReturn(false);
+
+    eventHandler.onChangeIndexed("test_project", changeId);
+    verifyZeroInteractions(changeChecker);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java
index 20b6da8..cd919f4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java
@@ -16,14 +16,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.DefaultSharedRefEnforcement;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.RefFixture;
@@ -64,11 +68,12 @@
   @Mock SharedRefDatabaseWrapper sharedRefDatabase;
 
   @Mock SharedRefEnforcement tmpRefEnforcement;
+  @Mock ProjectsFilter projectsFilter;
 
   @Before
   public void setup() throws Exception {
     super.setUp();
-
+    when(projectsFilter.matches(anyString())).thenReturn(true);
     gitRepoSetup();
   }
 
@@ -137,6 +142,24 @@
         (command) -> assertThat(command.getResult()).isEqualTo(ReceiveCommand.Result.LOCK_FAILURE));
   }
 
+  @Test
+  public void shouldNotUpdateSharedRefDbWhenProjectIsLocal() throws Exception {
+    when(projectsFilter.matches(anyString())).thenReturn(true);
+
+    String AN_OUT_OF_SYNC_REF = "refs/changes/01/1/1";
+    BatchRefUpdate batchRefUpdate =
+        newBatchUpdate(
+            Collections.singletonList(new ReceiveCommand(A, B, AN_OUT_OF_SYNC_REF, UPDATE)));
+    BatchRefUpdateValidator batchRefUpdateValidator =
+        getRefValidatorForEnforcement(A_TEST_PROJECT_NAME, tmpRefEnforcement);
+
+    batchRefUpdateValidator.executeBatchUpdateWithValidation(
+        batchRefUpdate, () -> execute(batchRefUpdate));
+
+    verify(sharedRefDatabase, never())
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+  }
+
   private BatchRefUpdateValidator newDefaultValidator(String projectName) {
     return getRefValidatorForEnforcement(projectName, new DefaultSharedRefEnforcement());
   }
@@ -148,6 +171,7 @@
         new ValidationMetrics(new DisabledMetricMaker()),
         sharedRefEnforcement,
         new DummyLockWrapper(),
+        projectsFilter,
         projectName,
         diskRepo.getRefDatabase());
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
index fcbafbb..a155752 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.multisite.validation;
 
 import static java.util.Arrays.asList;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
@@ -24,6 +25,7 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.DefaultSharedRefEnforcement;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.RefFixture;
@@ -38,6 +40,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
@@ -56,6 +59,7 @@
   @Mock RevWalk revWalk;
   @Mock ProgressMonitor progressMonitor;
   @Mock ValidationMetrics validationMetrics;
+  @Mock ProjectsFilter projectsFilter;
 
   private final Ref oldRef =
       new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_1);
@@ -91,6 +95,11 @@
     return "branch_" + nameRule.getMethodName();
   }
 
+  @Before
+  public void setup() {
+    when(projectsFilter.matches(anyString())).thenReturn(true);
+  }
+
   @SuppressWarnings("deprecation")
   private void setMockRequiredReturnValues() throws IOException {
 
@@ -167,6 +176,7 @@
                 validationMetrics,
                 new DefaultSharedRefEnforcement(),
                 new DummyLockWrapper(),
+                projectsFilter,
                 projectName,
                 refDb);
           }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdateTest.java
index d9e5c07..029a4c6 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdateTest.java
@@ -16,11 +16,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.validation.RefUpdateValidator.Factory;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.DefaultSharedRefEnforcement;
@@ -33,6 +36,7 @@
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
@@ -49,6 +53,7 @@
   @Mock SharedRefDatabaseWrapper sharedRefDb;
   @Mock ValidationMetrics validationMetrics;
   @Mock RefDatabase refDb;
+  @Mock ProjectsFilter projectsFilter;
 
   private final Ref oldRef =
       new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_1);
@@ -62,6 +67,11 @@
     return "branch_" + nameRule.getMethodName();
   }
 
+  @Before
+  public void setup() {
+    when(projectsFilter.matches(anyString())).thenReturn(true);
+  }
+
   @Test
   public void newUpdateShouldValidateAndSucceed() throws Exception {
 
@@ -182,6 +192,7 @@
                     validationMetrics,
                     new DefaultSharedRefEnforcement(),
                     new DummyLockWrapper(),
+                    projectsFilter,
                     projectName,
                     refDb);
             return RefUpdateValidator;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
index 2eefb5a..d437a73 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
@@ -26,6 +26,7 @@
 
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -33,6 +34,7 @@
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.RefFixture;
@@ -63,6 +65,7 @@
   @Mock SharedRefDatabaseWrapper sharedRefDb;
   @Mock GitReferenceUpdated gitReferenceUpdated;
   @Mock ProjectVersionLogger verLogger;
+  @Mock ProjectsFilter projectsFilter;
 
   @Inject private ProjectConfig.Factory projectConfigFactory;
   @Inject private InMemoryRepositoryManager repoManager;
@@ -73,6 +76,7 @@
 
   @Before
   public void setUp() throws Exception {
+    when(projectsFilter.matches(any(Event.class))).thenReturn(true);
     InMemoryRepository inMemoryRepo = repoManager.createRepository(A_TEST_PROJECT_NAME_KEY);
     project = projectConfigFactory.create(A_TEST_PROJECT_NAME_KEY);
     project.load(inMemoryRepo);
@@ -105,7 +109,8 @@
     when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
     when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
 
-    new ProjectVersionRefUpdate(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+    new ProjectVersionRefUpdate(
+            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, projectsFilter)
         .onEvent(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
@@ -150,7 +155,8 @@
     when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
     when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
 
-    new ProjectVersionRefUpdate(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+    new ProjectVersionRefUpdate(
+            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, projectsFilter)
         .onEvent(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
@@ -190,7 +196,8 @@
     when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
     when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
 
-    new ProjectVersionRefUpdate(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+    new ProjectVersionRefUpdate(
+            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, projectsFilter)
         .onEvent(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
@@ -227,7 +234,8 @@
     when(refUpdatedEvent.getRefName()).thenReturn(magicRefName);
     repo.branch(magicRefName).commit().create();
 
-    new ProjectVersionRefUpdate(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+    new ProjectVersionRefUpdate(
+            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, projectsFilter)
         .onEvent(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
@@ -242,7 +250,30 @@
     when(refUpdatedEvent.getProjectNameKey()).thenReturn(Project.nameKey("aNonExistentProject"));
     when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
 
-    new ProjectVersionRefUpdate(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+    new ProjectVersionRefUpdate(
+            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, projectsFilter)
+        .onEvent(refUpdatedEvent);
+
+    Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
+    assertThat(ref).isNull();
+
+    verifyZeroInteractions(verLogger);
+  }
+
+  @Test
+  public void shouldNotUpdateProjectVersionWhenProjectFilteredOut() throws Exception {
+    when(projectsFilter.matches(any(Event.class))).thenReturn(false);
+
+    Context.setForwardedEvent(false);
+
+    Thread.sleep(1000L);
+    repo.branch("master").commit().create();
+
+    Thread.sleep(1000L);
+    repo.branch("master").update(masterCommit);
+
+    new ProjectVersionRefUpdate(
+            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, projectsFilter)
         .onEvent(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
@@ -257,7 +288,8 @@
         .thenReturn(Optional.of("123"));
 
     Optional<Long> version =
-        new ProjectVersionRefUpdate(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        new ProjectVersionRefUpdate(
+                repoManager, sharedRefDb, gitReferenceUpdated, verLogger, projectsFilter)
             .getProjectRemoteVersion(A_TEST_PROJECT_NAME);
 
     assertThat(version.isPresent()).isTrue();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java
index 3f4d1bb..2951c89 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java
@@ -16,12 +16,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.entities.Project;
+import com.googlesource.gerrit.plugins.multisite.ProjectsFilter;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.SharedRefLogger;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.DefaultSharedRefEnforcement;
@@ -53,6 +56,8 @@
 
   @Mock RefUpdate refUpdate;
 
+  @Mock ProjectsFilter projectsFilter;
+
   String refName;
   Ref oldUpdateRef;
   Ref newUpdateRef;
@@ -74,6 +79,8 @@
     doReturn(refName).when(refUpdate).getName();
     lenient().doReturn(oldUpdateRef.getObjectId()).when(refUpdate).getOldObjectId();
 
+    doReturn(true).when(projectsFilter).matches(anyString());
+
     refUpdateValidator = newRefUpdateValidator(sharedRefDb);
   }
 
@@ -193,12 +200,23 @@
     assertThat(result).isEqualTo(RefUpdate.Result.LOCK_FAILURE);
   }
 
+  @Test
+  public void shouldNotUpdateSharedRefDbWhenProjectIsLocal() throws Exception {
+    when(projectsFilter.matches(anyString())).thenReturn(false);
+
+    refUpdateValidator.executeRefUpdate(refUpdate, () -> RefUpdate.Result.NEW);
+
+    verify(sharedRefDb, never())
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+  }
+
   private RefUpdateValidator newRefUpdateValidator(SharedRefDatabaseWrapper refDbWrapper) {
     return new RefUpdateValidator(
         refDbWrapper,
         validationMetrics,
         defaultRefEnforcement,
         new DummyLockWrapper(),
+        projectsFilter,
         A_TEST_PROJECT_NAME,
         localRefDb);
   }