Merge branch 'stable-3.2'

* stable-3.2:
  Set version to 3.2.3 for release

Change-Id: Idaf7c2ed5a601b6d091268147057b79899a4801a
diff --git a/Jenkinsfile b/Jenkinsfile
index a090baf..deb1d46 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -19,7 +19,7 @@
         stage('build') {
             steps {
                 gerritReview labels: [Verified: 0], message: "Build started: ${env.BUILD_URL}"
-                sh 'mvn test'
+                sh "bash -c '. set-java.sh 11 && mvn test'"
             }
         }
     }
diff --git a/pom.xml b/pom.xml
index 9ad8148..c67aee8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
     <groupId>com.gerritforge</groupId>
     <artifactId>global-refdb</artifactId>
-    <version>3.2.3</version>
+    <version>3.3.0-rc1</version>
     <packaging>jar</packaging>
 
     <name>global-refdb</name>
@@ -38,7 +38,7 @@
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <gerrit.version>3.2.3</gerrit.version>
+        <gerrit.version>3.3.0-rc1</gerrit.version>
     </properties>
 
     <dependencies>
@@ -75,8 +75,8 @@
                 <artifactId>maven-compiler-plugin</artifactId>
                 <version>3.6.1</version>
                 <configuration>
-                    <source>1.8</source>
-                    <target>1.8</target>
+                    <source>11</source>
+                    <target>11</target>
                 </configuration>
             </plugin>
             <plugin>
@@ -95,7 +95,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.1.1</version>
+                <version>3.2.0</version>
                 <executions>
                     <execution>
                         <id>attach-javadocs</id>
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java
new file mode 100644
index 0000000..0961b3c
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java
@@ -0,0 +1,178 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.OutOfSyncException;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+public class BatchRefUpdateValidator extends RefUpdateValidator {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static interface Factory {
+    BatchRefUpdateValidator create(String projectName, RefDatabase refDb);
+  }
+
+  public interface BatchValidationWrapper {
+    void apply(BatchRefUpdate batchRefUpdate, NoParameterVoidFunction arg) throws IOException;
+  }
+
+  @Inject
+  public BatchRefUpdateValidator(
+      SharedRefDatabaseWrapper sharedRefDb,
+      ValidationMetrics validationMetrics,
+      SharedRefEnforcement refEnforcement,
+      LockWrapper.Factory lockWrapperFactory,
+      ProjectsFilter projectsFilter,
+      @Assisted String projectName,
+      @Assisted RefDatabase refDb) {
+    super(
+        sharedRefDb,
+        validationMetrics,
+        refEnforcement,
+        lockWrapperFactory,
+        projectsFilter,
+        projectName,
+        refDb);
+  }
+
+  public void executeBatchUpdateWithValidation(
+      BatchRefUpdate batchRefUpdate, NoParameterVoidFunction batchRefUpdateFunction)
+      throws IOException {
+    if (refEnforcement.getPolicy(projectName) == EnforcePolicy.IGNORED
+        || !isGlobalProject(projectName)) {
+      batchRefUpdateFunction.invoke();
+      return;
+    }
+
+    try {
+      doExecuteBatchUpdate(batchRefUpdate, batchRefUpdateFunction);
+    } catch (IOException e) {
+      logger.atWarning().withCause(e).log(
+          "Failed to execute Batch Update on project %s", projectName);
+      if (refEnforcement.getPolicy(projectName) == EnforcePolicy.REQUIRED) {
+        throw e;
+      }
+    }
+  }
+
+  private void doExecuteBatchUpdate(
+      BatchRefUpdate batchRefUpdate, NoParameterVoidFunction delegateUpdate) throws IOException {
+
+    List<ReceiveCommand> commands = batchRefUpdate.getCommands();
+    if (commands.isEmpty()) {
+      return;
+    }
+
+    List<RefPair> refsToUpdate = getRefsPairs(commands).collect(Collectors.toList());
+    List<RefPair> refsFailures =
+        refsToUpdate.stream().filter(RefPair::hasFailed).collect(Collectors.toList());
+    if (!refsFailures.isEmpty()) {
+      String allFailuresMessage =
+          refsFailures.stream()
+              .map(refPair -> String.format("Failed to fetch ref %s", refPair.compareRef.getName()))
+              .collect(Collectors.joining(", "));
+      Exception firstFailureException = refsFailures.get(0).exception;
+
+      logger.atSevere().withCause(firstFailureException).log(allFailuresMessage);
+      throw new IOException(allFailuresMessage, firstFailureException);
+    }
+
+    try (CloseableSet<AutoCloseable> locks = new CloseableSet<>()) {
+      refsToUpdate = compareAndGetLatestLocalRefs(refsToUpdate, locks);
+      delegateUpdate.invoke();
+      updateSharedRefDb(batchRefUpdate.getCommands().stream(), refsToUpdate);
+    } catch (OutOfSyncException e) {
+      List<ReceiveCommand> receiveCommands = batchRefUpdate.getCommands();
+      logger.atWarning().withCause(e).log(
+          String.format(
+              "Batch ref-update failing because node is out of sync with the shared ref-db. Set all commands Result to LOCK_FAILURE [%d]",
+              receiveCommands.size()));
+      receiveCommands.forEach((command) -> command.setResult(ReceiveCommand.Result.LOCK_FAILURE));
+    }
+  }
+
+  private void updateSharedRefDb(Stream<ReceiveCommand> commandStream, List<RefPair> refsToUpdate)
+      throws IOException {
+    if (commandStream
+        .filter(cmd -> cmd.getResult() != ReceiveCommand.Result.OK)
+        .findFirst()
+        .isPresent()) {
+      return;
+    }
+
+    for (RefPair refPair : refsToUpdate) {
+      updateSharedDbOrThrowExceptionFor(refPair);
+    }
+  }
+
+  private Stream<RefPair> getRefsPairs(List<ReceiveCommand> receivedCommands) {
+    return receivedCommands.stream().map(this::getRefPairForCommand);
+  }
+
+  private RefPair getRefPairForCommand(ReceiveCommand command) {
+    try {
+      switch (command.getType()) {
+        case CREATE:
+          return new RefPair(nullRef(command.getRefName()), getNewRef(command));
+
+        case UPDATE:
+        case UPDATE_NONFASTFORWARD:
+          return new RefPair(getCurrentRef(command.getRefName()), getNewRef(command));
+
+        case DELETE:
+          return new RefPair(getCurrentRef(command.getRefName()), ObjectId.zeroId());
+
+        default:
+          return new RefPair(
+              command.getRef(),
+              new IllegalArgumentException("Unsupported command type " + command.getType()));
+      }
+    } catch (IOException e) {
+      return new RefPair(command.getRef(), e);
+    }
+  }
+
+  private ObjectId getNewRef(ReceiveCommand command) {
+    return command.getNewId();
+  }
+
+  private List<RefPair> compareAndGetLatestLocalRefs(
+      List<RefPair> refsToUpdate, CloseableSet<AutoCloseable> locks) throws IOException {
+    List<RefPair> latestRefsToUpdate = new ArrayList<>();
+    for (RefPair refPair : refsToUpdate) {
+      latestRefsToUpdate.add(compareAndGetLatestLocalRef(refPair, locks));
+    }
+    return latestRefsToUpdate;
+  }
+
+  private static final Ref nullRef(String refName) {
+    return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, refName, ObjectId.zeroId());
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModule.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModule.java
new file mode 100644
index 0000000..49b20d8
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModule.java
@@ -0,0 +1,37 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.NoopSharedRefDatabase;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.Scopes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LibModule extends LifecycleModule {
+  private static final Logger log = LoggerFactory.getLogger(LibModule.class);
+
+  @Override
+  protected void configure() {
+    DynamicItem.itemOf(binder(), GlobalRefDatabase.class);
+    DynamicItem.bind(binder(), GlobalRefDatabase.class)
+        .to(NoopSharedRefDatabase.class)
+        .in(Scopes.SINGLETON);
+
+    log.info("Shared ref-db engine: none");
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModuleLogFile.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModuleLogFile.java
new file mode 100644
index 0000000..075bcc9
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModuleLogFile.java
@@ -0,0 +1,32 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.gerrit.server.util.SystemLog;
+import org.apache.log4j.AsyncAppender;
+import org.apache.log4j.Layout;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+public abstract class LibModuleLogFile {
+
+  public LibModuleLogFile(SystemLog systemLog, String logName, Layout layout) {
+    AsyncAppender asyncAppender = systemLog.createAsyncAppender(logName, layout, true, true);
+    Logger logger = LogManager.getLogger(logName);
+    logger.removeAppender(logName);
+    logger.addAppender(asyncAppender);
+    logger.setAdditivity(false);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LockWrapper.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LockWrapper.java
new file mode 100644
index 0000000..18d75a3
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LockWrapper.java
@@ -0,0 +1,50 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class LockWrapper implements AutoCloseable {
+  public interface Factory {
+    LockWrapper create(
+        @Assisted("project") String project,
+        @Assisted("refName") String refName,
+        @Assisted AutoCloseable lock);
+  }
+
+  private final String project;
+  private final String refName;
+  private final AutoCloseable lock;
+  private final SharedRefLogger sharedRefLogger;
+
+  @Inject
+  public LockWrapper(
+      SharedRefLogger sharedRefLogger,
+      @Assisted("project") String project,
+      @Assisted("refName") String refName,
+      @Assisted AutoCloseable lock) {
+    this.lock = lock;
+    this.sharedRefLogger = sharedRefLogger;
+    this.project = project;
+    this.refName = refName;
+  }
+
+  @Override
+  public void close() throws Exception {
+    lock.close();
+    sharedRefLogger.logLockRelease(project, refName);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java
new file mode 100644
index 0000000..5653ee2
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java
@@ -0,0 +1,141 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.CommonConverters;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.util.SystemLog;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.apache.log4j.PatternLayout;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class Log4jSharedRefLogger extends LibModuleLogFile implements SharedRefLogger {
+  private static final String LOG_NAME = "sharedref_log";
+  private Logger sharedRefDBLog;
+  private final GitRepositoryManager gitRepositoryManager;
+  private static final Gson gson = OutputFormat.JSON_COMPACT.newGson();
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Inject
+  public Log4jSharedRefLogger(SystemLog systemLog, GitRepositoryManager gitRepositoryManager) {
+    super(systemLog, LOG_NAME, new PatternLayout("[%d{ISO8601}] [%t] %-5p : %m%n"));
+    this.gitRepositoryManager = gitRepositoryManager;
+    sharedRefDBLog = LoggerFactory.getLogger(LOG_NAME);
+  }
+
+  @Override
+  public void logRefUpdate(String project, Ref currRef, ObjectId newRefValue) {
+    if (!ObjectId.zeroId().equals(newRefValue)) {
+      try (Repository repository = gitRepositoryManager.openRepository(Project.nameKey(project));
+          RevWalk walk = new RevWalk(repository)) {
+        GitPerson committer = null;
+        String commitMessage = null;
+        if (newRefValue != null) {
+          int objectType = walk.parseAny(newRefValue).getType();
+          switch (objectType) {
+            case OBJ_COMMIT:
+              RevCommit commit = walk.parseCommit(newRefValue);
+              committer = CommonConverters.toGitPerson(commit.getCommitterIdent());
+              commitMessage = commit.getShortMessage();
+              break;
+            case OBJ_BLOB:
+              break;
+            default:
+              throw new IncorrectObjectTypeException(newRefValue, Constants.typeString(objectType));
+          }
+        }
+        sharedRefDBLog.info(
+            gson.toJson(
+                new SharedRefLogEntry.UpdateRef(
+                    project,
+                    currRef.getName(),
+                    currRef.getObjectId().getName(),
+                    newRefValue == null ? ObjectId.zeroId().name() : newRefValue.getName(),
+                    committer,
+                    commitMessage)));
+      } catch (IOException e) {
+        logger.atSevere().withCause(e).log(
+            "Cannot log sharedRefDB interaction for ref %s on project %s",
+            currRef.getName(), project);
+      }
+    } else {
+      sharedRefDBLog.info(
+          gson.toJson(
+              new SharedRefLogEntry.DeleteRef(
+                  project, currRef.getName(), currRef.getObjectId().getName())));
+    }
+  }
+
+  @Override
+  public <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue) {
+    if (newRefValue != null) {
+      sharedRefDBLog.info(
+          gson.toJson(
+              new SharedRefLogEntry.UpdateRef(
+                  project, refName, safeToString(currRef), safeToString(newRefValue), null, null)));
+    } else {
+      sharedRefDBLog.info(
+          gson.toJson(new SharedRefLogEntry.DeleteRef(project, refName, safeToString(currRef))));
+    }
+  }
+
+  @Override
+  public void logProjectDelete(String project) {
+    sharedRefDBLog.info(gson.toJson(new SharedRefLogEntry.DeleteProject(project)));
+  }
+
+  @Override
+  public void logLockAcquisition(String project, String refName) {
+    sharedRefDBLog.info(gson.toJson(new SharedRefLogEntry.LockAcquire(project, refName)));
+  }
+
+  @Override
+  public void logLockRelease(String project, String refName) {
+    sharedRefDBLog.info(gson.toJson(new SharedRefLogEntry.LockRelease(project, refName)));
+  }
+
+  @VisibleForTesting
+  public void setLogger(Logger logger) {
+    this.sharedRefDBLog = logger;
+  }
+
+  private <T> String safeToString(T currRef) {
+    if (currRef == null) {
+      return "<null>";
+    }
+    return currRef.toString();
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanup.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanup.java
new file mode 100644
index 0000000..ec72bcc
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanup.java
@@ -0,0 +1,53 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.inject.Inject;
+
+public class ProjectDeletedSharedDbCleanup implements ProjectDeletedListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SharedRefDatabaseWrapper sharedDb;
+
+  private final ValidationMetrics validationMetrics;
+
+  @Inject
+  public ProjectDeletedSharedDbCleanup(
+      SharedRefDatabaseWrapper sharedDb, ValidationMetrics validationMetrics) {
+    this.sharedDb = sharedDb;
+    this.validationMetrics = validationMetrics;
+  }
+
+  @Override
+  public void onProjectDeleted(Event event) {
+    String projectName = event.getProjectName();
+    logger.atInfo().log(
+        "Deleting project '%s'. Will perform a cleanup in Shared-Ref database.", projectName);
+
+    try {
+      sharedDb.remove(Project.nameKey(projectName));
+    } catch (GlobalRefDbSystemError e) {
+      validationMetrics.incrementSplitBrain();
+      logger.atSevere().withCause(e).log(
+          "Project '%s' deleted from GIT but it was not able to cleanup"
+              + " from Shared-Ref database",
+          projectName);
+    }
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilter.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilter.java
new file mode 100644
index 0000000..3121df4
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilter.java
@@ -0,0 +1,112 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+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;
+
+// import com.google.gerrit.entities.AccessSection;
+
+@Singleton
+public class ProjectsFilter {
+  public enum PatternType {
+    REGEX,
+    WILDCARD,
+    EXACT_MATCH;
+
+    public static PatternType getPatternType(String pattern) {
+      // TODO: Use AccessSection
+      if (pattern.startsWith("^")) {
+        //      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(SharedRefDbConfiguration 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/gerritforge/gerrit/globalrefdb/validation/RefPair.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java
new file mode 100644
index 0000000..7156a7e
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java
@@ -0,0 +1,47 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public class RefPair {
+  public final Ref compareRef;
+  public final ObjectId putValue;
+  public final Exception exception;
+
+  RefPair(Ref oldRef, ObjectId newRefValue) {
+    if (oldRef == null) {
+      throw new IllegalArgumentException("Required not-null ref in RefPair");
+    }
+    this.compareRef = oldRef;
+    this.putValue = newRefValue;
+    this.exception = null;
+  }
+
+  RefPair(Ref newRef, Exception e) {
+    this.compareRef = newRef;
+    this.exception = e;
+    this.putValue = ObjectId.zeroId();
+  }
+
+  public String getName() {
+    return compareRef.getName();
+  }
+
+  public boolean hasFailed() {
+    return exception != null;
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java
new file mode 100644
index 0000000..123e382
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java
@@ -0,0 +1,295 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.OutOfSyncException;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedDbSplitBrainException;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedLockException;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
+import com.google.common.base.MoreObjects;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.HashMap;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+
+public class RefUpdateValidator {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static final String SHARED_REF_DB_VERSIONING_REF = "refs/global-refdb/version";
+
+  protected final SharedRefDatabaseWrapper sharedRefDb;
+  protected final ValidationMetrics validationMetrics;
+
+  protected final String projectName;
+  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);
+  }
+
+  public interface ExceptionThrowingSupplier<T, E extends Exception> {
+    T create() throws E;
+  }
+
+  public interface RefValidationWrapper {
+    RefUpdate.Result apply(NoParameterFunction<RefUpdate.Result> arg, RefUpdate refUpdate)
+        throws IOException;
+  }
+
+  public interface NoParameterFunction<T> {
+    T invoke() throws IOException;
+  }
+
+  public interface NoParameterVoidFunction {
+    void invoke() throws IOException;
+  }
+
+  @Inject
+  public RefUpdateValidator(
+      SharedRefDatabaseWrapper sharedRefDb,
+      ValidationMetrics validationMetrics,
+      SharedRefEnforcement refEnforcement,
+      LockWrapper.Factory lockWrapperFactory,
+      ProjectsFilter projectsFilter,
+      @Assisted String projectName,
+      @Assisted RefDatabase refDb) {
+    this.sharedRefDb = sharedRefDb;
+    this.validationMetrics = validationMetrics;
+    this.lockWrapperFactory = lockWrapperFactory;
+    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();
+    }
+
+    try {
+      return doExecuteRefUpdate(refUpdate, refUpdateFunction);
+    } catch (SharedDbSplitBrainException e) {
+      validationMetrics.incrementSplitBrain();
+
+      logger.atWarning().withCause(e).log(
+          "Unable to execute ref-update on project=%s ref=%s",
+          projectName, refUpdate.getRef().getName());
+      if (refEnforcement.getPolicy(projectName) == EnforcePolicy.REQUIRED) {
+        throw e;
+      }
+    }
+    return null;
+  }
+
+  private Boolean isProjectVersionUpdate(String refName) {
+    Boolean isProjectVersionUpdate = refName.equals(SHARED_REF_DB_VERSIONING_REF);
+    logger.atFine().log("Is project version update? " + isProjectVersionUpdate);
+    return isProjectVersionUpdate;
+  }
+
+  private <T extends Throwable> void softFailBasedOnEnforcement(T e, EnforcePolicy policy)
+      throws T {
+    logger.atWarning().withCause(e).log(
+        String.format(
+            "Failure while running with policy enforcement %s. Error message: %s",
+            policy, e.getMessage()));
+    if (policy == EnforcePolicy.REQUIRED) {
+      throw e;
+    }
+  }
+
+  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 {
+    try (CloseableSet<AutoCloseable> locks = new CloseableSet<>()) {
+      RefPair refPairForUpdate = newRefPairFrom(refUpdate);
+      compareAndGetLatestLocalRef(refPairForUpdate, locks);
+      RefUpdate.Result result = refUpdateFunction.invoke();
+      if (isSuccessful(result)) {
+        updateSharedDbOrThrowExceptionFor(refPairForUpdate);
+      }
+      return result;
+    } catch (OutOfSyncException e) {
+      logger.atWarning().withCause(e).log(
+          String.format("Local node is out of sync with ref-db: %s", e.getMessage()));
+
+      return RefUpdate.Result.LOCK_FAILURE;
+    }
+  }
+
+  protected void updateSharedDbOrThrowExceptionFor(RefPair refPair) throws IOException {
+    // We are not checking refs that should be ignored
+    final EnforcePolicy refEnforcementPolicy =
+        refEnforcement.getPolicy(projectName, refPair.getName());
+    if (refEnforcementPolicy == EnforcePolicy.IGNORED) return;
+
+    String errorMessage =
+        String.format(
+            "Not able to persist the data in Zookeeper for project '%s' and ref '%s',"
+                + "the cluster is now in Split Brain since the commit has been "
+                + "persisted locally but not in SharedRef the value %s",
+            projectName, refPair.getName(), refPair.putValue);
+    boolean succeeded;
+    try {
+      succeeded =
+          sharedRefDb.compareAndPut(
+              Project.nameKey(projectName), refPair.compareRef, refPair.putValue);
+    } catch (GlobalRefDbSystemError e) {
+      logger.atWarning().withCause(e).log(
+          "Not able to persist the data in Zookeeper for project '{}' and ref '{}', message: {}",
+          projectName,
+          refPair.getName(),
+          e.getMessage());
+      throw new SharedDbSplitBrainException(errorMessage, e);
+    }
+
+    if (!succeeded) {
+      throw new SharedDbSplitBrainException(errorMessage);
+    }
+  }
+
+  protected RefPair compareAndGetLatestLocalRef(RefPair refPair, CloseableSet<AutoCloseable> locks)
+      throws SharedLockException, OutOfSyncException, IOException {
+    String refName = refPair.getName();
+    EnforcePolicy refEnforcementPolicy = refEnforcement.getPolicy(projectName, refName);
+    if (refEnforcementPolicy == EnforcePolicy.IGNORED) {
+      return refPair;
+    }
+
+    locks.addResourceIfNotExist(
+        String.format("%s-%s", projectName, refName),
+        () ->
+            lockWrapperFactory.create(
+                projectName, refName, sharedRefDb.lockRef(Project.nameKey(projectName), refName)));
+
+    RefPair latestRefPair = getLatestLocalRef(refPair);
+    if (sharedRefDb.isUpToDate(Project.nameKey(projectName), latestRefPair.compareRef)) {
+      return latestRefPair;
+    }
+
+    if (isNullRef(latestRefPair.compareRef)
+        || sharedRefDb.exists(Project.nameKey(projectName), refName)) {
+      validationMetrics.incrementSplitBrainPrevention();
+
+      softFailBasedOnEnforcement(
+          new OutOfSyncException(projectName, latestRefPair.compareRef), refEnforcementPolicy);
+    }
+
+    return latestRefPair;
+  }
+
+  private boolean isNullRef(Ref ref) {
+    return ref.getObjectId().equals(ObjectId.zeroId());
+  }
+
+  private RefPair getLatestLocalRef(RefPair refPair) throws IOException {
+    Ref latestRef = refDb.exactRef(refPair.getName());
+    return new RefPair(
+        latestRef == null ? nullRef(refPair.getName()) : latestRef, refPair.putValue);
+  }
+
+  private Ref nullRef(String name) {
+    return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, name, ObjectId.zeroId());
+  }
+
+  protected boolean isSuccessful(RefUpdate.Result result) {
+    switch (result) {
+      case NEW:
+      case FORCED:
+      case FAST_FORWARD:
+      case NO_CHANGE:
+      case RENAMED:
+        return true;
+
+      case REJECTED_OTHER_REASON:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_CURRENT_BRANCH:
+      case NOT_ATTEMPTED:
+      case LOCK_FAILURE:
+      case IO_FAILURE:
+      case REJECTED:
+      default:
+        return false;
+    }
+  }
+
+  protected RefPair newRefPairFrom(RefUpdate refUpdate) throws IOException {
+    return new RefPair(getCurrentRef(refUpdate.getName()), refUpdate.getNewObjectId());
+  }
+
+  protected Ref getCurrentRef(String refName) throws IOException {
+    return MoreObjects.firstNonNull(refDb.findRef(refName), nullRef(refName));
+  }
+
+  public static class CloseableSet<T extends AutoCloseable> implements AutoCloseable {
+    private final HashMap<String, AutoCloseable> elements;
+
+    public CloseableSet() {
+      this(new HashMap<>());
+    }
+
+    public CloseableSet(HashMap<String, AutoCloseable> elements) {
+      this.elements = elements;
+    }
+
+    public void addResourceIfNotExist(
+        String key, ExceptionThrowingSupplier<T, SharedLockException> resourceFactory)
+        throws SharedLockException {
+      if (!elements.containsKey(key)) {
+        elements.put(key, resourceFactory.create());
+      }
+    }
+
+    @Override
+    public void close() {
+      elements.values().stream()
+          .forEach(
+              closeable -> {
+                try {
+                  closeable.close();
+                } catch (Exception closingException) {
+                  logger.atSevere().withCause(closingException).log(
+                      "Exception trying to release resource %s, "
+                          + "the locked resources won't be accessible in all cluster unless"
+                          + " the lock is removed from ZK manually",
+                      closeable);
+                }
+              });
+    }
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
new file mode 100644
index 0000000..d9c8051
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
@@ -0,0 +1,102 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.NoopSharedRefDatabase;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.inject.Inject;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public class SharedRefDatabaseWrapper implements GlobalRefDatabase {
+  private static final GlobalRefDatabase NOOP_REFDB = new NoopSharedRefDatabase();
+
+  @Inject(optional = true)
+  private DynamicItem<GlobalRefDatabase> sharedRefDbDynamicItem;
+
+  private final SharedRefLogger sharedRefLogger;
+
+  @Inject
+  public SharedRefDatabaseWrapper(SharedRefLogger sharedRefLogger) {
+    this.sharedRefLogger = sharedRefLogger;
+  }
+
+  @VisibleForTesting
+  public SharedRefDatabaseWrapper(
+      DynamicItem<GlobalRefDatabase> sharedRefDbDynamicItem, SharedRefLogger sharedRefLogger) {
+    this.sharedRefLogger = sharedRefLogger;
+    this.sharedRefDbDynamicItem = sharedRefDbDynamicItem;
+  }
+
+  @Override
+  public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException {
+    return sharedRefDb().isUpToDate(project, ref);
+  }
+
+  @Override
+  public boolean compareAndPut(Project.NameKey project, Ref currRef, ObjectId newRefValue)
+      throws GlobalRefDbSystemError {
+    boolean succeeded = sharedRefDb().compareAndPut(project, currRef, newRefValue);
+    if (succeeded) {
+      sharedRefLogger.logRefUpdate(project.get(), currRef, newRefValue);
+    }
+    return succeeded;
+  }
+
+  @Override
+  public <T> boolean compareAndPut(Project.NameKey project, String refName, T currValue, T newValue)
+      throws GlobalRefDbSystemError {
+    boolean succeeded = sharedRefDb().compareAndPut(project, refName, currValue, newValue);
+    if (succeeded) {
+      sharedRefLogger.logRefUpdate(project.get(), refName, currValue, newValue);
+    }
+    return succeeded;
+  }
+
+  @Override
+  public AutoCloseable lockRef(Project.NameKey project, String refName)
+      throws GlobalRefDbLockException {
+    AutoCloseable locker = sharedRefDb().lockRef(project, refName);
+    sharedRefLogger.logLockAcquisition(project.get(), refName);
+    return locker;
+  }
+
+  @Override
+  public boolean exists(Project.NameKey project, String refName) {
+    return sharedRefDb().exists(project, refName);
+  }
+
+  @Override
+  public void remove(Project.NameKey project) throws GlobalRefDbSystemError {
+    sharedRefDb().remove(project);
+    sharedRefLogger.logProjectDelete(project.get());
+  }
+
+  @Override
+  public <T> Optional<T> get(Project.NameKey nameKey, String s, Class<T> clazz)
+      throws GlobalRefDbSystemError {
+    return sharedRefDb().get(nameKey, s, clazz);
+  }
+
+  private GlobalRefDatabase sharedRefDb() {
+    return Optional.ofNullable(sharedRefDbDynamicItem).map(di -> di.get()).orElse(NOOP_REFDB);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdate.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdate.java
new file mode 100644
index 0000000..5d23c32
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdate.java
@@ -0,0 +1,185 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
+
+public class SharedRefDbBatchRefUpdate extends BatchRefUpdate {
+
+  private final BatchRefUpdate batchRefUpdate;
+  private final String project;
+  private final BatchRefUpdateValidator.Factory batchRefValidatorFactory;
+  private final RefDatabase refDb;
+
+  public static interface Factory {
+    SharedRefDbBatchRefUpdate create(String project, RefDatabase refDb);
+  }
+
+  @Inject
+  public SharedRefDbBatchRefUpdate(
+      BatchRefUpdateValidator.Factory batchRefValidatorFactory,
+      @Assisted String project,
+      @Assisted RefDatabase refDb) {
+    super(refDb);
+    this.refDb = refDb;
+    this.project = project;
+    this.batchRefUpdate = refDb.newBatchUpdate();
+    this.batchRefValidatorFactory = batchRefValidatorFactory;
+  }
+
+  @Override
+  public int hashCode() {
+    return batchRefUpdate.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return batchRefUpdate.equals(obj);
+  }
+
+  @Override
+  public boolean isAllowNonFastForwards() {
+    return batchRefUpdate.isAllowNonFastForwards();
+  }
+
+  @Override
+  public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
+    return batchRefUpdate.setAllowNonFastForwards(allow);
+  }
+
+  @Override
+  public PersonIdent getRefLogIdent() {
+    return batchRefUpdate.getRefLogIdent();
+  }
+
+  @Override
+  public BatchRefUpdate setRefLogIdent(PersonIdent pi) {
+    return batchRefUpdate.setRefLogIdent(pi);
+  }
+
+  @Override
+  public String getRefLogMessage() {
+    return batchRefUpdate.getRefLogMessage();
+  }
+
+  @Override
+  public boolean isRefLogIncludingResult() {
+    return batchRefUpdate.isRefLogIncludingResult();
+  }
+
+  @Override
+  public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
+    return batchRefUpdate.setRefLogMessage(msg, appendStatus);
+  }
+
+  @Override
+  public BatchRefUpdate disableRefLog() {
+    return batchRefUpdate.disableRefLog();
+  }
+
+  @Override
+  public BatchRefUpdate setForceRefLog(boolean force) {
+    return batchRefUpdate.setForceRefLog(force);
+  }
+
+  @Override
+  public boolean isRefLogDisabled() {
+    return batchRefUpdate.isRefLogDisabled();
+  }
+
+  @Override
+  public BatchRefUpdate setAtomic(boolean atomic) {
+    return batchRefUpdate.setAtomic(atomic);
+  }
+
+  @Override
+  public boolean isAtomic() {
+    return batchRefUpdate.isAtomic();
+  }
+
+  @Override
+  public void setPushCertificate(PushCertificate cert) {
+    batchRefUpdate.setPushCertificate(cert);
+  }
+
+  @Override
+  public List<ReceiveCommand> getCommands() {
+    return batchRefUpdate.getCommands();
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(ReceiveCommand cmd) {
+    return batchRefUpdate.addCommand(cmd);
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
+    return batchRefUpdate.addCommand(cmd);
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
+    return batchRefUpdate.addCommand(cmd);
+  }
+
+  @Override
+  public List<String> getPushOptions() {
+    return batchRefUpdate.getPushOptions();
+  }
+
+  @Override
+  public List<ProposedTimestamp> getProposedTimestamps() {
+    return batchRefUpdate.getProposedTimestamps();
+  }
+
+  @Override
+  public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) {
+    return batchRefUpdate.addProposedTimestamp(ts);
+  }
+
+  @Override
+  public void execute(RevWalk walk, ProgressMonitor monitor, List<String> options)
+      throws IOException {
+    batchRefValidatorFactory
+        .create(project, refDb)
+        .executeBatchUpdateWithValidation(
+            batchRefUpdate, () -> batchRefUpdate.execute(walk, monitor, options));
+  }
+
+  @Override
+  public void execute(RevWalk walk, ProgressMonitor monitor) throws IOException {
+    batchRefValidatorFactory
+        .create(project, refDb)
+        .executeBatchUpdateWithValidation(
+            batchRefUpdate, () -> batchRefUpdate.execute(walk, monitor));
+  }
+
+  @Override
+  public String toString() {
+    return batchRefUpdate.toString();
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbConfiguration.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbConfiguration.java
new file mode 100644
index 0000000..3708f74
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbConfiguration.java
@@ -0,0 +1,127 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import static com.google.common.base.Suppliers.memoize;
+import static com.google.common.base.Suppliers.ofInstance;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SharedRefDbConfiguration {
+  private static final Logger log = LoggerFactory.getLogger(SharedRefDbConfiguration.class);
+
+  private final Supplier<Projects> projects;
+  private final Supplier<SharedRefDatabase> sharedRefDb;
+
+  public SharedRefDbConfiguration(Config config) {
+    Supplier<Config> lazyCfg = lazyLoad(config);
+    projects = memoize(() -> new Projects(lazyCfg));
+    sharedRefDb = memoize(() -> new SharedRefDatabase(lazyCfg));
+  }
+
+  public SharedRefDatabase getSharedRefDb() {
+    return sharedRefDb.get();
+  }
+
+  public Projects projects() {
+    return projects.get();
+  }
+
+  private Supplier<Config> lazyLoad(Config config) {
+    if (config instanceof FileBasedConfig) {
+      return memoize(
+          () -> {
+            FileBasedConfig fileConfig = (FileBasedConfig) config;
+            String fileConfigFileName = fileConfig.getFile().getPath();
+            try {
+              log.info("Loading configuration from {}", fileConfigFileName);
+              fileConfig.load();
+            } catch (IOException | ConfigInvalidException e) {
+              log.error("Unable to load configuration from " + fileConfigFileName, e);
+            }
+            return fileConfig;
+          });
+    }
+    return ofInstance(config);
+  }
+
+  public static class SharedRefDatabase {
+    public static final String SECTION = "ref-database";
+    public static final String ENABLE_KEY = "enabled";
+    public static final String SUBSECTION_ENFORCEMENT_RULES = "enforcementRules";
+
+    private final boolean enabled;
+    private final Multimap<EnforcePolicy, String> enforcementRules;
+
+    private SharedRefDatabase(Supplier<Config> cfg) {
+      enabled = getBoolean(cfg, SECTION, null, ENABLE_KEY, false);
+
+      enforcementRules = MultimapBuilder.hashKeys().arrayListValues().build();
+      for (EnforcePolicy policy : EnforcePolicy.values()) {
+        enforcementRules.putAll(
+            policy, getList(cfg, SECTION, SUBSECTION_ENFORCEMENT_RULES, policy.name()));
+      }
+    }
+
+    public boolean isEnabled() {
+      return enabled;
+    }
+
+    public Multimap<EnforcePolicy, String> getEnforcementRules() {
+      return enforcementRules;
+    }
+
+    private List<String> getList(
+        Supplier<Config> cfg, String section, String subsection, String name) {
+      return ImmutableList.copyOf(cfg.get().getStringList(section, subsection, name));
+    }
+  }
+
+  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(SECTION, null, PATTERN_KEY));
+    }
+
+    public List<String> getPatterns() {
+      return patterns;
+    }
+  }
+
+  static boolean getBoolean(
+      Supplier<Config> cfg, String section, String subsection, String name, boolean defaultValue) {
+    try {
+      return cfg.get().getBoolean(section, subsection, name, defaultValue);
+    } catch (IllegalArgumentException e) {
+      log.error("invalid value for {}; using default value {}", name, defaultValue);
+      log.debug("Failed to retrieve boolean value: {}", e.getMessage(), e);
+      return defaultValue;
+    }
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManager.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManager.java
new file mode 100644
index 0000000..b533c05
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManager.java
@@ -0,0 +1,61 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.SortedSet;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+public class SharedRefDbGitRepositoryManager implements GitRepositoryManager {
+  private final GitRepositoryManager gitRepositoryManager;
+  private final SharedRefDbRepository.Factory sharedRefDbRepoFactory;
+
+  @Inject
+  public SharedRefDbGitRepositoryManager(
+      SharedRefDbRepository.Factory sharedRefDbRepoFactory,
+      LocalDiskRepositoryManager localDiskRepositoryManager) {
+    this.sharedRefDbRepoFactory = sharedRefDbRepoFactory;
+    this.gitRepositoryManager = localDiskRepositoryManager;
+  }
+
+  @Override
+  public Repository openRepository(Project.NameKey name)
+      throws RepositoryNotFoundException, IOException {
+    return wrap(name, gitRepositoryManager.openRepository(name));
+  }
+
+  @Override
+  public Repository createRepository(Project.NameKey name)
+      throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException {
+    return wrap(name, gitRepositoryManager.createRepository(name));
+  }
+
+  @Override
+  public SortedSet<Project.NameKey> list() {
+    return gitRepositoryManager.list();
+  }
+
+  private Repository wrap(Project.NameKey projectName, Repository projectRepo) {
+    return sharedRefDbRepoFactory.create(projectName.get(), projectRepo);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabase.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabase.java
new file mode 100644
index 0000000..8597c49
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabase.java
@@ -0,0 +1,160 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+
+public class SharedRefDbRefDatabase extends RefDatabase {
+  private final SharedRefDbRefUpdate.Factory refUpdateFactory;
+  private final SharedRefDbBatchRefUpdate.Factory batchRefUpdateFactory;
+  private final String projectName;
+  private final RefDatabase refDatabase;
+
+  public interface Factory {
+    public SharedRefDbRefDatabase create(String projectName, RefDatabase refDatabase);
+  }
+
+  @Inject
+  public SharedRefDbRefDatabase(
+      SharedRefDbRefUpdate.Factory refUpdateFactory,
+      SharedRefDbBatchRefUpdate.Factory batchRefUpdateFactory,
+      @Assisted String projectName,
+      @Assisted RefDatabase refDatabase) {
+    this.refUpdateFactory = refUpdateFactory;
+    this.batchRefUpdateFactory = batchRefUpdateFactory;
+    this.projectName = projectName;
+    this.refDatabase = refDatabase;
+  }
+
+  @Override
+  public int hashCode() {
+    return refDatabase.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return refDatabase.equals(obj);
+  }
+
+  @Override
+  public void create() throws IOException {
+    refDatabase.create();
+  }
+
+  @Override
+  public void close() {
+    refDatabase.close();
+  }
+
+  @Override
+  public boolean isNameConflicting(String name) throws IOException {
+    return refDatabase.isNameConflicting(name);
+  }
+
+  @Override
+  public Collection<String> getConflictingNames(String name) throws IOException {
+    return refDatabase.getConflictingNames(name);
+  }
+
+  @Override
+  public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+    return wrapRefUpdate(refDatabase.newUpdate(name, detach));
+  }
+
+  @Override
+  public RefRename newRename(String fromName, String toName) throws IOException {
+    return refDatabase.newRename(fromName, toName);
+  }
+
+  @Override
+  public BatchRefUpdate newBatchUpdate() {
+    return batchRefUpdateFactory.create(projectName, refDatabase);
+  }
+
+  @Override
+  public boolean performsAtomicTransactions() {
+    return refDatabase.performsAtomicTransactions();
+  }
+
+  @Override
+  public String toString() {
+    return refDatabase.toString();
+  }
+
+  @Override
+  public Ref exactRef(String name) throws IOException {
+    return refDatabase.exactRef(name);
+  }
+
+  @Override
+  public Map<String, Ref> exactRef(String... refs) throws IOException {
+    return refDatabase.exactRef(refs);
+  }
+
+  @Override
+  public Ref firstExactRef(String... refs) throws IOException {
+    return refDatabase.firstExactRef(refs);
+  }
+
+  @Override
+  public List<Ref> getRefs() throws IOException {
+    return refDatabase.getRefs();
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public Map<String, Ref> getRefs(String prefix) throws IOException {
+    return refDatabase.getRefs(prefix);
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefix(String prefix) throws IOException {
+    return refDatabase.getRefsByPrefix(prefix);
+  }
+
+  @Override
+  public boolean hasRefs() throws IOException {
+    return refDatabase.hasRefs();
+  }
+
+  @Override
+  public List<Ref> getAdditionalRefs() throws IOException {
+    return refDatabase.getAdditionalRefs();
+  }
+
+  @Override
+  public Ref peel(Ref ref) throws IOException {
+    return refDatabase.peel(ref);
+  }
+
+  @Override
+  public void refresh() {
+    refDatabase.refresh();
+  }
+
+  RefUpdate wrapRefUpdate(RefUpdate refUpdate) {
+    return refUpdateFactory.create(projectName, refUpdate, refDatabase);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefUpdate.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefUpdate.java
new file mode 100644
index 0000000..eb68b14
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefUpdate.java
@@ -0,0 +1,237 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+
+public class SharedRefDbRefUpdate extends RefUpdate {
+
+  protected final RefUpdate refUpdateBase;
+  private final String projectName;
+  private final RefUpdateValidator.Factory refValidatorFactory;
+  private final RefUpdateValidator refUpdateValidator;
+
+  public interface Factory {
+    SharedRefDbRefUpdate create(String projectName, RefUpdate refUpdate, RefDatabase refDb);
+  }
+
+  @Inject
+  public SharedRefDbRefUpdate(
+      RefUpdateValidator.Factory refValidatorFactory,
+      @Assisted String projectName,
+      @Assisted RefUpdate refUpdate,
+      @Assisted RefDatabase refDb) {
+    super(refUpdate.getRef());
+    refUpdateBase = refUpdate;
+    this.projectName = projectName;
+    this.refValidatorFactory = refValidatorFactory;
+    refUpdateValidator = this.refValidatorFactory.create(this.projectName, refDb);
+  }
+
+  @Override
+  protected RefDatabase getRefDatabase() {
+    return notImplementedException();
+  }
+
+  private <T> T notImplementedException() {
+    throw new IllegalStateException("This method should have never been invoked");
+  }
+
+  @Override
+  protected Repository getRepository() {
+    return notImplementedException();
+  }
+
+  @Override
+  protected boolean tryLock(boolean deref) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  protected void unlock() {
+    notImplementedException();
+  }
+
+  @Override
+  protected Result doUpdate(Result result) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  protected Result doDelete(Result result) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  protected Result doLink(String target) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  public Result update() throws IOException {
+    return refUpdateValidator.executeRefUpdate(refUpdateBase, refUpdateBase::update);
+  }
+
+  @Override
+  public Result update(RevWalk rev) throws IOException {
+    return refUpdateValidator.executeRefUpdate(refUpdateBase, () -> refUpdateBase.update(rev));
+  }
+
+  @Override
+  public Result delete() throws IOException {
+    return refUpdateValidator.executeRefUpdate(refUpdateBase, refUpdateBase::delete);
+  }
+
+  @Override
+  public Result delete(RevWalk walk) throws IOException {
+    return refUpdateValidator.executeRefUpdate(refUpdateBase, () -> refUpdateBase.delete(walk));
+  }
+
+  @Override
+  public int hashCode() {
+    return refUpdateBase.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return refUpdateBase.equals(obj);
+  }
+
+  @Override
+  public String toString() {
+    return refUpdateBase.toString();
+  }
+
+  @Override
+  public String getName() {
+    return refUpdateBase.getName();
+  }
+
+  @Override
+  public Ref getRef() {
+    return refUpdateBase.getRef();
+  }
+
+  @Override
+  public ObjectId getNewObjectId() {
+    return refUpdateBase.getNewObjectId();
+  }
+
+  @Override
+  public void setDetachingSymbolicRef() {
+    refUpdateBase.setDetachingSymbolicRef();
+  }
+
+  @Override
+  public boolean isDetachingSymbolicRef() {
+    return refUpdateBase.isDetachingSymbolicRef();
+  }
+
+  @Override
+  public void setNewObjectId(AnyObjectId id) {
+    refUpdateBase.setNewObjectId(id);
+  }
+
+  @Override
+  public ObjectId getExpectedOldObjectId() {
+    return refUpdateBase.getExpectedOldObjectId();
+  }
+
+  @Override
+  public void setExpectedOldObjectId(AnyObjectId id) {
+    refUpdateBase.setExpectedOldObjectId(id);
+  }
+
+  @Override
+  public boolean isForceUpdate() {
+    return refUpdateBase.isForceUpdate();
+  }
+
+  @Override
+  public void setForceUpdate(boolean b) {
+    refUpdateBase.setForceUpdate(b);
+  }
+
+  @Override
+  public PersonIdent getRefLogIdent() {
+    return refUpdateBase.getRefLogIdent();
+  }
+
+  @Override
+  public void setRefLogIdent(PersonIdent pi) {
+    refUpdateBase.setRefLogIdent(pi);
+  }
+
+  @Override
+  public String getRefLogMessage() {
+    return refUpdateBase.getRefLogMessage();
+  }
+
+  @Override
+  public void setRefLogMessage(String msg, boolean appendStatus) {
+    refUpdateBase.setRefLogMessage(msg, appendStatus);
+  }
+
+  @Override
+  public void disableRefLog() {
+    refUpdateBase.disableRefLog();
+  }
+
+  @Override
+  public void setForceRefLog(boolean force) {
+    refUpdateBase.setForceRefLog(force);
+  }
+
+  @Override
+  public ObjectId getOldObjectId() {
+    return refUpdateBase.getOldObjectId();
+  }
+
+  @Override
+  public void setPushCertificate(PushCertificate cert) {
+    refUpdateBase.setPushCertificate(cert);
+  }
+
+  @Override
+  public Result getResult() {
+    return refUpdateBase.getResult();
+  }
+
+  @Override
+  public Result forceUpdate() throws IOException {
+    return refUpdateValidator.executeRefUpdate(refUpdateBase, refUpdateBase::forceUpdate);
+  }
+
+  @Override
+  public Result link(String target) throws IOException {
+    return refUpdateBase.link(target);
+  }
+
+  @Override
+  public void setCheckConflicting(boolean check) {
+    refUpdateBase.setCheckConflicting(check);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepository.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepository.java
new file mode 100644
index 0000000..cdb65da
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepository.java
@@ -0,0 +1,56 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.gerrit.server.git.DelegateRepository;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+public class SharedRefDbRepository extends DelegateRepository {
+
+  private final SharedRefDbRefDatabase sharedRefDatabase;
+
+  public interface Factory {
+    public SharedRefDbRepository create(String projectName, Repository repository);
+  }
+
+  @Inject
+  public SharedRefDbRepository(
+      SharedRefDbRefDatabase.Factory refDbFactory,
+      @Assisted String projectName,
+      @Assisted Repository repository) {
+    super(repository);
+    this.sharedRefDatabase = refDbFactory.create(projectName, repository.getRefDatabase());
+  }
+
+  @Override
+  public RefDatabase getRefDatabase() {
+    return sharedRefDatabase;
+  }
+
+  @Override
+  public RefUpdate updateRef(String ref) throws IOException {
+    return sharedRefDatabase.wrapRefUpdate(delegate.updateRef(ref));
+  }
+
+  @Override
+  public RefUpdate updateRef(String ref, boolean detach) throws IOException {
+    return sharedRefDatabase.wrapRefUpdate(delegate.updateRef(ref, detach));
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java
new file mode 100644
index 0000000..d580057
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java
@@ -0,0 +1,100 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.GitPerson;
+
+public class SharedRefLogEntry {
+
+  public enum Type {
+    UPDATE_REF,
+    DELETE_REF,
+    DELETE_PROJECT,
+    LOCK_ACQUIRE,
+    LOCK_RELEASE
+  }
+
+  public String projectName;
+  public Type type;
+
+  public static class UpdateRef extends SharedRefLogEntry {
+
+    public String refName;
+    public String oldId;
+    public String newId;
+    public GitPerson committer;
+    public String comment;
+
+    UpdateRef(
+        String projectName,
+        String refName,
+        String oldId,
+        String newId,
+        @Nullable GitPerson committer,
+        @Nullable String comment) {
+      this.type = Type.UPDATE_REF;
+      this.projectName = projectName;
+      this.refName = refName;
+      this.oldId = oldId;
+      this.newId = newId;
+      this.committer = committer;
+      this.comment = comment;
+    }
+  }
+
+  public static class DeleteProject extends SharedRefLogEntry {
+
+    DeleteProject(String projectName) {
+      this.type = Type.DELETE_PROJECT;
+      this.projectName = projectName;
+    }
+  }
+
+  public static class DeleteRef extends SharedRefLogEntry {
+
+    public String refName;
+    public String oldId;
+
+    DeleteRef(String projectName, String refName, String oldId) {
+      this.type = Type.DELETE_REF;
+      this.projectName = projectName;
+      this.refName = refName;
+      this.oldId = oldId;
+    }
+  }
+
+  public static class LockAcquire extends SharedRefLogEntry {
+
+    public String refName;
+
+    LockAcquire(String projectName, String refName) {
+      this.type = Type.LOCK_ACQUIRE;
+      this.projectName = projectName;
+      this.refName = refName;
+    }
+  }
+
+  public static class LockRelease extends SharedRefLogEntry {
+
+    public String refName;
+
+    LockRelease(String projectName, String refName) {
+      this.type = Type.LOCK_RELEASE;
+      this.projectName = projectName;
+      this.refName = refName;
+    }
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java
new file mode 100644
index 0000000..faef27b
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java
@@ -0,0 +1,31 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public interface SharedRefLogger {
+
+  void logRefUpdate(String project, Ref currRef, ObjectId newRefValue);
+
+  <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue);
+
+  void logProjectDelete(String project);
+
+  void logLockAcquisition(String project, String refName);
+
+  void logLockRelease(String project, String refName);
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ValidationMetrics.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ValidationMetrics.java
new file mode 100644
index 0000000..678091b
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ValidationMetrics.java
@@ -0,0 +1,72 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.PluginMetadata;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ValidationMetrics {
+  private static final String GIT_UPDATE_SPLIT_BRAIN_PREVENTED = "git_update_split_brain_prevented";
+  private static final String GIT_UPDATE_SPLIT_BRAIN = "git_update_split_brain";
+
+  private final Counter1<String> splitBrainPreventionCounter;
+  private final Counter1<String> splitBrainCounter;
+
+  @Inject
+  public ValidationMetrics(MetricMaker metricMaker) {
+    this.splitBrainPreventionCounter =
+        metricMaker.newCounter(
+            "global-refdb/validation/git_update_split_brain_prevented",
+            rateDescription("errors", "Rate of REST API error responses"),
+            stringField(
+                GIT_UPDATE_SPLIT_BRAIN_PREVENTED,
+                "Ref-update operations, split-brain detected and prevented"));
+
+    this.splitBrainCounter =
+        metricMaker.newCounter(
+            "global-refdb/validation/git_update_split_brain",
+            rateDescription("errors", "Rate of REST API error responses"),
+            stringField(
+                GIT_UPDATE_SPLIT_BRAIN,
+                "Ref-update operation left node in a split-brain scenario"));
+  }
+
+  public void incrementSplitBrainPrevention() {
+    splitBrainPreventionCounter.increment(GIT_UPDATE_SPLIT_BRAIN_PREVENTED);
+  }
+
+  public void incrementSplitBrain() {
+    splitBrainCounter.increment(GIT_UPDATE_SPLIT_BRAIN);
+  }
+
+  public Field<String> stringField(String metadataKey, String description) {
+    return Field.ofString(
+            metadataKey,
+            (metadataBuilder, fieldValue) ->
+                metadataBuilder.addPluginMetadata(PluginMetadata.create(metadataKey, fieldValue)))
+        .description(description)
+        .build();
+  }
+
+  public Description rateDescription(String unit, String description) {
+    return new Description(description).setRate().setUnit(unit);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java
new file mode 100644
index 0000000..b1eca36
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java
@@ -0,0 +1,103 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import static com.google.common.base.Suppliers.memoize;
+
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Splitter;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class CustomSharedRefEnforcementByProject implements SharedRefEnforcement {
+  private static final String ALL = ".*";
+
+  private final Supplier<Map<String, Map<String, EnforcePolicy>>> predefEnforcements;
+
+  @Inject
+  public CustomSharedRefEnforcementByProject(SharedRefDbConfiguration config) {
+    this.predefEnforcements = memoize(() -> parseDryRunEnforcementsToMap(config));
+  }
+
+  private static Map<String, Map<String, EnforcePolicy>> parseDryRunEnforcementsToMap(
+      SharedRefDbConfiguration config) {
+    Map<String, Map<String, EnforcePolicy>> enforcementMap = new HashMap<>();
+
+    for (Map.Entry<EnforcePolicy, String> enforcementEntry :
+        config.getSharedRefDb().getEnforcementRules().entries()) {
+      parseEnforcementEntry(enforcementMap, enforcementEntry);
+    }
+
+    return enforcementMap;
+  }
+
+  private static void parseEnforcementEntry(
+      Map<String, Map<String, EnforcePolicy>> enforcementMap,
+      Map.Entry<EnforcePolicy, String> enforcementEntry) {
+    Iterator<String> projectAndRef = Splitter.on(':').split(enforcementEntry.getValue()).iterator();
+    EnforcePolicy enforcementPolicy = enforcementEntry.getKey();
+
+    if (projectAndRef.hasNext()) {
+      String projectName = emptyToAll(projectAndRef.next());
+      String refName = emptyToAll(projectAndRef.hasNext() ? projectAndRef.next() : ALL);
+
+      Map<String, EnforcePolicy> existingOrDefaultRef =
+          enforcementMap.getOrDefault(projectName, new HashMap<>());
+
+      existingOrDefaultRef.put(refName, enforcementPolicy);
+
+      enforcementMap.put(projectName, existingOrDefaultRef);
+    }
+  }
+
+  private static String emptyToAll(String value) {
+    return value.trim().isEmpty() ? ALL : value;
+  }
+
+  @Override
+  public EnforcePolicy getPolicy(String projectName, String refName) {
+    if (isRefToBeIgnoredBySharedRefDb(refName)) {
+      return EnforcePolicy.IGNORED;
+    }
+
+    return getRefEnforcePolicy(projectName, refName);
+  }
+
+  private EnforcePolicy getRefEnforcePolicy(String projectName, String refName) {
+    Map<String, EnforcePolicy> orDefault =
+        predefEnforcements
+            .get()
+            .getOrDefault(
+                projectName, predefEnforcements.get().getOrDefault(ALL, ImmutableMap.of()));
+
+    return MoreObjects.firstNonNull(
+        orDefault.getOrDefault(refName, orDefault.get(ALL)), EnforcePolicy.REQUIRED);
+  }
+
+  @Override
+  public EnforcePolicy getPolicy(String projectName) {
+    Map<String, EnforcePolicy> policiesForProject =
+        predefEnforcements
+            .get()
+            .getOrDefault(
+                projectName, predefEnforcements.get().getOrDefault(ALL, ImmutableMap.of()));
+    return policiesForProject.getOrDefault(ALL, EnforcePolicy.REQUIRED);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcement.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcement.java
new file mode 100644
index 0000000..9c2ef35
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcement.java
@@ -0,0 +1,28 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+public class DefaultSharedRefEnforcement implements SharedRefEnforcement {
+
+  @Override
+  public EnforcePolicy getPolicy(String projectName, String refName) {
+    return isRefToBeIgnoredBySharedRefDb(refName) ? EnforcePolicy.IGNORED : EnforcePolicy.REQUIRED;
+  }
+
+  @Override
+  public EnforcePolicy getPolicy(String projectName) {
+    return EnforcePolicy.REQUIRED;
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java
new file mode 100644
index 0000000..4810c64
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java
@@ -0,0 +1,63 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
+import com.google.gerrit.entities.Project;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public class NoopSharedRefDatabase implements GlobalRefDatabase {
+
+  @Override
+  public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException {
+    return true;
+  }
+
+  @Override
+  public boolean compareAndPut(Project.NameKey project, Ref currRef, ObjectId newRefValue)
+      throws GlobalRefDbSystemError {
+    return true;
+  }
+
+  @Override
+  public <T> boolean compareAndPut(Project.NameKey project, String refName, T currValue, T newValue)
+      throws GlobalRefDbSystemError {
+    return false;
+  }
+
+  @Override
+  public AutoCloseable lockRef(Project.NameKey project, String refName)
+      throws GlobalRefDbLockException {
+    return () -> {};
+  }
+
+  @Override
+  public boolean exists(Project.NameKey project, String refName) {
+    return false;
+  }
+
+  @Override
+  public void remove(Project.NameKey project) throws GlobalRefDbSystemError {}
+
+  @Override
+  public <T> Optional<T> get(Project.NameKey project, String refName, Class<T> clazz)
+      throws GlobalRefDbSystemError {
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/OutOfSyncException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/OutOfSyncException.java
new file mode 100644
index 0000000..c8dc304
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/OutOfSyncException.java
@@ -0,0 +1,34 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import java.io.IOException;
+import org.eclipse.jgit.lib.Ref;
+
+/** Local project/ref is out of sync with the shared refdb */
+public class OutOfSyncException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  public OutOfSyncException(String project, Ref localRef) {
+    super(
+        localRef == null
+            ? String.format(
+                "Local ref doesn't exists locally for project %s but exists in the shared ref-db",
+                project)
+            : String.format(
+                "Local ref %s (ObjectId=%s) on project %s is out of sync with the shared ref-db",
+                localRef.getName(), localRef.getObjectId().getName(), project));
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedDbSplitBrainException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedDbSplitBrainException.java
new file mode 100644
index 0000000..aece4cb
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedDbSplitBrainException.java
@@ -0,0 +1,29 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import java.io.IOException;
+
+public class SharedDbSplitBrainException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  public SharedDbSplitBrainException(String message) {
+    super(message);
+  }
+
+  public SharedDbSplitBrainException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java
new file mode 100644
index 0000000..69c3f33
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java
@@ -0,0 +1,26 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import java.io.IOException;
+
+/** Unable to lock a project/ref resource. */
+public class SharedLockException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  public SharedLockException(String project, String refName, Exception cause) {
+    super(String.format("Unable to lock project %s on ref %s", project, refName), cause);
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedRefEnforcement.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedRefEnforcement.java
new file mode 100644
index 0000000..d2c1ed6
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedRefEnforcement.java
@@ -0,0 +1,53 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+/** Type of enforcement to implement between the local and shared RefDb. */
+public interface SharedRefEnforcement {
+  public enum EnforcePolicy {
+    IGNORED,
+    REQUIRED;
+  }
+
+  /**
+   * Get the enforcement policy for a project/refName.
+   *
+   * @param projectName project to be enforced
+   * @param refName ref name to be enforced
+   * @return the {@link EnforcePolicy} value
+   */
+  public EnforcePolicy getPolicy(String projectName, String refName);
+
+  /**
+   * Get the enforcement policy for a project
+   *
+   * @param projectName
+   * @return the {@link EnforcePolicy} value
+   */
+  public EnforcePolicy getPolicy(String projectName);
+
+  /**
+   * Check if a refName should be ignored by shared Ref-Db
+   *
+   * @param refName
+   * @return true if ref should be ignored; false otherwise
+   */
+  default boolean isRefToBeIgnoredBySharedRefDb(String refName) {
+    return refName == null
+        || refName.startsWith("refs/draft-comments")
+        || (refName.startsWith("refs/changes") && !refName.endsWith("/meta"))
+        || refName.startsWith("refs/cache-automerge");
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidatorTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidatorTest.java
new file mode 100644
index 0000000..e1c8478
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidatorTest.java
@@ -0,0 +1,194 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.jgit.internal.storage.file.RefDirectory;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class BatchRefUpdateValidatorTest extends LocalDiskRepositoryTestCase implements RefFixture {
+  @Rule public TestName nameRule = new TestName();
+
+  private Repository diskRepo;
+  private TestRepository<Repository> repo;
+  private RefDirectory refdir;
+  private RevCommit A;
+  private RevCommit B;
+
+  @Mock SharedRefDatabaseWrapper sharedRefDatabase;
+
+  @Mock SharedRefEnforcement tmpRefEnforcement;
+  @Mock ProjectsFilter projectsFilter;
+
+  @Before
+  public void setup() throws Exception {
+    super.setUp();
+    when(projectsFilter.matches(anyString())).thenReturn(true);
+    gitRepoSetup();
+  }
+
+  private void gitRepoSetup() throws Exception {
+    diskRepo = createBareRepository();
+    refdir = (RefDirectory) diskRepo.getRefDatabase();
+    repo = new TestRepository<>(diskRepo);
+    A = repo.commit().create();
+    B = repo.commit(repo.getRevWalk().parseCommit(A));
+  }
+
+  @Test
+  public void immutableChangeShouldNotBeWrittenIntoZk() throws Exception {
+    String AN_IMMUTABLE_REF = "refs/changes/01/1/1";
+
+    List<ReceiveCommand> cmds = Arrays.asList(new ReceiveCommand(A, B, AN_IMMUTABLE_REF, UPDATE));
+
+    BatchRefUpdate batchRefUpdate = newBatchUpdate(cmds);
+    BatchRefUpdateValidator BatchRefUpdateValidator = newDefaultValidator(A_TEST_PROJECT_NAME);
+
+    BatchRefUpdateValidator.executeBatchUpdateWithValidation(
+        batchRefUpdate, () -> execute(batchRefUpdate));
+
+    verify(sharedRefDatabase, never())
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+  }
+
+  @Test
+  public void compareAndPutShouldAlwaysIngoreAlwaysDraftCommentsEvenOutOfOrder() throws Exception {
+    String DRAFT_COMMENT = "refs/draft-comments/56/450756/1013728";
+    List<ReceiveCommand> cmds = Arrays.asList(new ReceiveCommand(A, B, DRAFT_COMMENT, UPDATE));
+
+    BatchRefUpdate batchRefUpdate = newBatchUpdate(cmds);
+    BatchRefUpdateValidator BatchRefUpdateValidator = newDefaultValidator(A_TEST_PROJECT_NAME);
+
+    BatchRefUpdateValidator.executeBatchUpdateWithValidation(
+        batchRefUpdate, () -> execute(batchRefUpdate));
+
+    verify(sharedRefDatabase, never())
+        .compareAndPut(A_TEST_PROJECT_NAME_KEY, newRef(DRAFT_COMMENT, A.getId()), B.getId());
+  }
+
+  @Test
+  public void validationShouldFailWhenLocalRefDbIsOutOfSync() throws Exception {
+    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);
+
+    doReturn(SharedRefEnforcement.EnforcePolicy.REQUIRED)
+        .when(batchRefUpdateValidator.refEnforcement)
+        .getPolicy(A_TEST_PROJECT_NAME, AN_OUT_OF_SYNC_REF);
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDatabase)
+        .isUpToDate(A_TEST_PROJECT_NAME_KEY, newRef(AN_OUT_OF_SYNC_REF, AN_OBJECT_ID_1));
+
+    batchRefUpdateValidator.executeBatchUpdateWithValidation(
+        batchRefUpdate, () -> execute(batchRefUpdate));
+
+    final List<ReceiveCommand> commands = batchRefUpdate.getCommands();
+    assertThat(commands.size()).isEqualTo(1);
+    commands.forEach(
+        (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());
+  }
+
+  private BatchRefUpdateValidator getRefValidatorForEnforcement(
+      String projectName, SharedRefEnforcement sharedRefEnforcement) {
+    return new BatchRefUpdateValidator(
+        sharedRefDatabase,
+        new ValidationMetrics(new DisabledMetricMaker()),
+        sharedRefEnforcement,
+        new DummyLockWrapper(),
+        projectsFilter,
+        projectName,
+        diskRepo.getRefDatabase());
+  }
+
+  private Void execute(BatchRefUpdate u) throws IOException {
+    try (RevWalk rw = new RevWalk(diskRepo)) {
+      u.execute(rw, NullProgressMonitor.INSTANCE);
+    }
+    return null;
+  }
+
+  private BatchRefUpdate newBatchUpdate(List<ReceiveCommand> cmds) {
+    BatchRefUpdate u = refdir.newBatchUpdate();
+    u.addCommand(cmds);
+    return u;
+  }
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java
new file mode 100644
index 0000000..f9a1e57
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java
@@ -0,0 +1,38 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Ignore;
+
+@Ignore
+public class DisabledSharedRefLogger implements SharedRefLogger {
+
+  @Override
+  public void logRefUpdate(String project, Ref currRef, ObjectId newRefValue) {}
+
+  @Override
+  public void logProjectDelete(String project) {}
+
+  @Override
+  public void logLockAcquisition(String project, String refName) {}
+
+  @Override
+  public void logLockRelease(String project, String refName) {}
+
+  @Override
+  public <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue) {}
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DummyLockWrapper.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DummyLockWrapper.java
new file mode 100644
index 0000000..84d6912
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DummyLockWrapper.java
@@ -0,0 +1,26 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import org.junit.Ignore;
+
+@Ignore
+public class DummyLockWrapper implements LockWrapper.Factory {
+
+  @Override
+  public LockWrapper create(String project, String refName, AutoCloseable lock) {
+    return new LockWrapper(new DisabledSharedRefLogger(), project, refName, lock);
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLoggerTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLoggerTest.java
new file mode 100644
index 0000000..2c89091
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLoggerTest.java
@@ -0,0 +1,165 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.util.SystemLog;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.WriterAppender;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Log4jSharedRefLoggerTest extends AbstractDaemonTest {
+
+  private static final Gson gson = OutputFormat.JSON_COMPACT.newGson();
+  private StringWriter logWriter;
+  private Log4jSharedRefLogger log4jSharedRefLogger;
+
+  @Before
+  public void setUp() throws IOException {
+    this.logWriter = new StringWriter();
+    this.log4jSharedRefLogger = newLog4jSharedRefLogger();
+  }
+
+  @Test
+  public void shouldLogProjectDeletion() {
+    log4jSharedRefLogger.logProjectDelete(project.get());
+
+    SharedRefLogEntry.DeleteProject gotLogEntry =
+        gson.fromJson(logWriter.toString(), SharedRefLogEntry.DeleteProject.class);
+
+    assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.DELETE_PROJECT);
+    assertThat(gotLogEntry.projectName).isEqualTo(project.get());
+  }
+
+  @Test
+  public void shouldLogUpdateRef() throws Exception {
+    final String refName = "refs/remotes/origin/master";
+    Ref currRef = repo().exactRef(refName);
+    PushOneCommit.Result result = pushTo(refName);
+    ObjectId newRefValue = result.getCommit().toObjectId();
+
+    log4jSharedRefLogger.logRefUpdate(project.get(), currRef, newRefValue);
+
+    SharedRefLogEntry.UpdateRef gotLogEntry =
+        gson.fromJson(logWriter.toString(), SharedRefLogEntry.UpdateRef.class);
+
+    assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.UPDATE_REF);
+    assertThat(gotLogEntry.projectName).isEqualTo(project.get());
+    assertThat(gotLogEntry.refName).isEqualTo(refName);
+    assertThat(gotLogEntry.oldId).isEqualTo(currRef.getObjectId().getName());
+    assertThat(gotLogEntry.newId).isEqualTo(newRefValue.getName());
+    assertThat(gotLogEntry.comment).isNotNull();
+    assertThat(gotLogEntry.committer).isNotNull();
+  }
+
+  @Test
+  public void shouldLogDeleteRef() throws Exception {
+    final String refName = "refs/remotes/origin/master";
+    Ref currRef = repo().exactRef(refName);
+
+    log4jSharedRefLogger.logRefUpdate(project.get(), currRef, ObjectId.zeroId());
+
+    SharedRefLogEntry.DeleteRef gotLogEntry =
+        gson.fromJson(logWriter.toString(), SharedRefLogEntry.DeleteRef.class);
+
+    assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.DELETE_REF);
+    assertThat(gotLogEntry.projectName).isEqualTo(project.get());
+    assertThat(gotLogEntry.refName).isEqualTo(refName);
+    assertThat(gotLogEntry.oldId).isEqualTo(currRef.getObjectId().getName());
+  }
+
+  @Test
+  public void shouldLogBlobRefs() throws Exception {
+    Repository allUsersRepo = repoManager.openRepository(allUsers);
+    String blobRefName = RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS;
+    Ref currRef = allUsersRepo.exactRef(blobRefName);
+    log4jSharedRefLogger.logRefUpdate(allUsers.get(), currRef, currRef.getObjectId());
+
+    SharedRefLogEntry.UpdateRef gotLogEntry =
+        gson.fromJson(logWriter.toString(), SharedRefLogEntry.UpdateRef.class);
+
+    assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.UPDATE_REF);
+    assertThat(gotLogEntry.projectName).isEqualTo(allUsers.get());
+    assertThat(gotLogEntry.refName).isEqualTo(blobRefName);
+    assertThat(gotLogEntry.oldId).isEqualTo(currRef.getObjectId().getName());
+    assertThat(gotLogEntry.newId).isEqualTo(currRef.getObjectId().getName());
+    assertThat(gotLogEntry.comment).isNull();
+    assertThat(gotLogEntry.committer).isNull();
+  }
+
+  @Test
+  public void shouldLogLockAcquisition() {
+    String refName = "refs/foo/bar";
+    log4jSharedRefLogger.logLockAcquisition(project.get(), refName);
+
+    SharedRefLogEntry.LockAcquire gotLogEntry =
+        gson.fromJson(logWriter.toString(), SharedRefLogEntry.LockAcquire.class);
+
+    assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.LOCK_ACQUIRE);
+    assertThat(gotLogEntry.projectName).isEqualTo(project.get());
+    assertThat(gotLogEntry.refName).isEqualTo(refName);
+  }
+
+  @Test
+  public void shouldLogLockRelease() {
+    String refName = "refs/foo/bar";
+    log4jSharedRefLogger.logLockRelease(project.get(), refName);
+
+    SharedRefLogEntry.LockAcquire gotLogEntry =
+        gson.fromJson(logWriter.toString(), SharedRefLogEntry.LockAcquire.class);
+
+    assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.LOCK_RELEASE);
+    assertThat(gotLogEntry.projectName).isEqualTo(project.get());
+    assertThat(gotLogEntry.refName).isEqualTo(refName);
+  }
+
+  private Log4jSharedRefLogger newLog4jSharedRefLogger() throws IOException {
+    final Log4jSharedRefLogger log4jSharedRefLogger =
+        new Log4jSharedRefLogger(new SystemLog(new SitePaths(newPath()), baseConfig), repoManager);
+    log4jSharedRefLogger.setLogger(logWriterLogger());
+    return log4jSharedRefLogger;
+  }
+
+  private Logger logWriterLogger() {
+    org.apache.log4j.Logger logger = LogManager.getLogger("logWriterLogger");
+    logger.addAppender(new WriterAppender(new PatternLayout("%m"), logWriter));
+    return LoggerFactory.getLogger("logWriterLogger");
+  }
+
+  private static Path newPath() throws IOException {
+    Path tmp = Files.createTempFile("gerrit_test_", "_site");
+    Files.deleteIfExists(tmp);
+    return tmp;
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanupTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanupTest.java
new file mode 100644
index 0000000..cf383bf
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanupTest.java
@@ -0,0 +1,58 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ProjectDeletedSharedDbCleanupTest implements RefFixture {
+  @Rule public TestName nameRule = new TestName();
+
+  @Mock ValidationMetrics mockValidationMetrics;
+  @Mock SharedRefDatabaseWrapper sharedRefDatabase;
+
+  @Test
+  public void aDeleteProjectEventShouldCleanupProjectFromZk() throws Exception {
+    String projectName = A_TEST_PROJECT_NAME;
+    ProjectDeletedSharedDbCleanup projectDeletedSharedDbCleanup =
+        new ProjectDeletedSharedDbCleanup(sharedRefDatabase, mockValidationMetrics);
+
+    ProjectDeletedListener.Event event =
+        new ProjectDeletedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return projectName;
+          }
+
+          @Override
+          public NotifyHandling getNotify() {
+            return NotifyHandling.NONE;
+          }
+        };
+
+    projectDeletedSharedDbCleanup.onProjectDeleted(event);
+
+    Mockito.verify(sharedRefDatabase, Mockito.times(1)).remove(A_TEST_PROJECT_NAME_KEY);
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilterTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilterTest.java
new file mode 100644
index 0000000..a8da17e
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/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.gerritforge.gerrit.globalrefdb.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration.Projects;
+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 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 SharedRefDbConfiguration 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/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java
new file mode 100644
index 0000000..2c7431f
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java
@@ -0,0 +1,218 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedDbSplitBrainException;
+import com.google.gerrit.entities.Project;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+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.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RefUpdateValidatorTest implements RefFixture {
+  private static final DefaultSharedRefEnforcement defaultRefEnforcement =
+      new DefaultSharedRefEnforcement();
+
+  @Mock SharedRefDatabaseWrapper sharedRefDb;
+
+  @Mock SharedRefLogger sharedRefLogger;
+
+  @Mock RefDatabase localRefDb;
+
+  @Mock ValidationMetrics validationMetrics;
+
+  @Mock RefUpdate refUpdate;
+
+  @Mock ProjectsFilter projectsFilter;
+
+  String refName;
+  Ref oldUpdateRef;
+  Ref newUpdateRef;
+  Ref localRef;
+
+  RefUpdateValidator refUpdateValidator;
+
+  @Before
+  public void setupMocks() throws Exception {
+    refName = aBranchRef();
+    oldUpdateRef = newRef(refName, AN_OBJECT_ID_1);
+    newUpdateRef = newRef(refName, AN_OBJECT_ID_2);
+    localRef = newRef(refName, AN_OBJECT_ID_3);
+
+    doReturn(localRef).when(localRefDb).findRef(refName);
+    doReturn(localRef).when(localRefDb).exactRef(refName);
+    doReturn(oldUpdateRef).when(refUpdate).getRef();
+    doReturn(newUpdateRef.getObjectId()).when(refUpdate).getNewObjectId();
+    doReturn(refName).when(refUpdate).getName();
+    lenient().doReturn(oldUpdateRef.getObjectId()).when(refUpdate).getOldObjectId();
+
+    doReturn(true).when(projectsFilter).matches(anyString());
+
+    refUpdateValidator = newRefUpdateValidator(sharedRefDb);
+  }
+
+  @Test
+  public void validationShouldSucceedWhenSharedRefDbIsNoop() throws Exception {
+    SharedRefDatabaseWrapper noopSharedRefDbWrapper = new SharedRefDatabaseWrapper(sharedRefLogger);
+
+    Result result =
+        newRefUpdateValidator(noopSharedRefDbWrapper).executeRefUpdate(refUpdate, () -> Result.NEW);
+    assertThat(result).isEqualTo(Result.NEW);
+  }
+
+  @Test
+  public void validationShouldSucceedWhenLocalRefDbIsUpToDate() throws Exception {
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDb)
+        .isUpToDate(any(Project.NameKey.class), any(Ref.class));
+    doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef);
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDb)
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+    doReturn(true)
+        .when(sharedRefDb)
+        .compareAndPut(A_TEST_PROJECT_NAME_KEY, localRef, newUpdateRef.getObjectId());
+
+    Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW);
+
+    assertThat(result).isEqualTo(Result.NEW);
+  }
+
+  @Test
+  public void sharedRefDbShouldBeUpdatedWithRefDeleted() throws Exception {
+    doReturn(ObjectId.zeroId()).when(refUpdate).getNewObjectId();
+    doReturn(true).when(sharedRefDb).isUpToDate(any(Project.NameKey.class), any(Ref.class));
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDb)
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+    doReturn(true)
+        .when(sharedRefDb)
+        .compareAndPut(A_TEST_PROJECT_NAME_KEY, localRef, ObjectId.zeroId());
+    doReturn(localRef).doReturn(null).when(localRefDb).findRef(refName);
+
+    Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.FORCED);
+
+    assertThat(result).isEqualTo(Result.FORCED);
+  }
+
+  @Test
+  public void sharedRefDbShouldBeUpdatedWithNewRefCreated() throws Exception {
+    Ref localNullRef = nullRef(refName);
+
+    doReturn(true).when(sharedRefDb).isUpToDate(any(Project.NameKey.class), any(Ref.class));
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDb)
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+    doReturn(true)
+        .when(sharedRefDb)
+        .compareAndPut(A_TEST_PROJECT_NAME_KEY, localNullRef, newUpdateRef.getObjectId());
+    doReturn(localNullRef).doReturn(newUpdateRef).when(localRefDb).findRef(refName);
+
+    Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW);
+
+    assertThat(result).isEqualTo(Result.NEW);
+  }
+
+  @Test
+  public void validationShouldFailWhenLocalRefDbIsOutOfSync() throws Exception {
+    lenient()
+        .doReturn(true)
+        .when(sharedRefDb)
+        .isUpToDate(any(Project.NameKey.class), any(Ref.class));
+    doReturn(true).when(sharedRefDb).exists(A_TEST_PROJECT_NAME_KEY, refName);
+    doReturn(false).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef);
+
+    Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW);
+
+    assertThat(result).isEqualTo(Result.LOCK_FAILURE);
+  }
+
+  @Test(expected = SharedDbSplitBrainException.class)
+  public void shouldTrowSplitBrainWhenLocalRefDbIsUpToDateButFinalCompareAndPutIsFailing()
+      throws Exception {
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDb)
+        .isUpToDate(any(Project.NameKey.class), any(Ref.class));
+    doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef);
+    lenient()
+        .doReturn(true)
+        .when(sharedRefDb)
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+    doReturn(false)
+        .when(sharedRefDb)
+        .compareAndPut(A_TEST_PROJECT_NAME_KEY, localRef, newUpdateRef.getObjectId());
+
+    refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW);
+  }
+
+  @Test
+  public void shouldNotUpdateSharedRefDbWhenFinalCompareAndPutIsFailing() throws Exception {
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDb)
+        .isUpToDate(any(Project.NameKey.class), any(Ref.class));
+    doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef);
+
+    Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.LOCK_FAILURE);
+
+    verify(sharedRefDb, never())
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+    assertThat(result).isEqualTo(Result.LOCK_FAILURE);
+  }
+
+  @Test
+  public void shouldNotUpdateSharedRefDbWhenProjectIsLocal() throws Exception {
+    when(projectsFilter.matches(anyString())).thenReturn(false);
+
+    refUpdateValidator.executeRefUpdate(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);
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdateTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdateTest.java
new file mode 100644
index 0000000..e52cc5f
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdateTest.java
@@ -0,0 +1,211 @@
+// 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.gerritforge.gerrit.globalrefdb.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;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture;
+import java.io.IOException;
+import java.util.Collections;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+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;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SharedRefDbBatchRefUpdateTest implements RefFixture {
+
+  @Mock SharedRefDatabaseWrapper sharedRefDb;
+  @Mock BatchRefUpdate batchRefUpdate;
+  @Mock BatchRefUpdateValidator batchRefUpdateValidator;
+  @Mock RefDatabase refDatabase;
+  @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);
+  private final Ref newRef =
+      new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_2);
+  ReceiveCommand receiveCommandBeforeExecution =
+      createReceiveCommand(
+          oldRef.getObjectId(), newRef.getObjectId(), oldRef.getName(), Result.NOT_ATTEMPTED);
+
+  ReceiveCommand successReceiveCommandAfterExecution =
+      createReceiveCommand(oldRef.getObjectId(), newRef.getObjectId(), oldRef.getName(), Result.OK);
+
+  ReceiveCommand rejectReceiveCommandAfterExecution =
+      createReceiveCommand(
+          oldRef.getObjectId(),
+          newRef.getObjectId(),
+          oldRef.getName(),
+          Result.REJECTED_NONFASTFORWARD);
+
+  private ReceiveCommand createReceiveCommand(
+      ObjectId oldRefObjectId, ObjectId newRefObjectId, String refName, Result result) {
+    ReceiveCommand receiveCommand = new ReceiveCommand(oldRefObjectId, newRefObjectId, refName);
+    receiveCommand.setResult(result);
+    return receiveCommand;
+  }
+
+  private SharedRefDbBatchRefUpdate sharedRefDbRefUpdate;
+
+  @Rule public TestName nameRule = new TestName();
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  @Before
+  public void setup() {
+    when(projectsFilter.matches(anyString())).thenReturn(true);
+  }
+
+  @SuppressWarnings("deprecation")
+  private void setMockRequiredReturnValues() throws IOException {
+
+    doReturn(batchRefUpdate).when(refDatabase).newBatchUpdate();
+
+    when(batchRefUpdate.getCommands())
+        .thenReturn(asList(receiveCommandBeforeExecution))
+        .thenReturn(asList(successReceiveCommandAfterExecution));
+
+    doReturn(oldRef).when(refDatabase).getRef(A_TEST_REF_NAME);
+    doReturn(oldRef).when(refDatabase).exactRef(A_TEST_REF_NAME);
+
+    sharedRefDbRefUpdate = getSharedRefDbBatchRefUpdateWithDefaultPolicyEnforcement();
+
+    verifyZeroInteractions(validationMetrics);
+  }
+
+  @Test
+  public void executeAndDelegateSuccessfullyWithNoExceptions() throws Exception {
+    setMockRequiredReturnValues();
+
+    // When compareAndPut against sharedDb succeeds
+    doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, oldRef);
+    doReturn(true)
+        .when(sharedRefDb)
+        .compareAndPut(eq(A_TEST_PROJECT_NAME_KEY), refEquals(oldRef), eq(newRef.getObjectId()));
+    sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+    verify(sharedRefDb)
+        .compareAndPut(eq(A_TEST_PROJECT_NAME_KEY), refEquals(oldRef), eq(newRef.getObjectId()));
+  }
+
+  private Ref refEquals(Ref oldRef) {
+    return argThat(new RefMatcher(oldRef));
+  }
+
+  @Test(expected = IOException.class)
+  public void executeAndFailsWithExceptions() throws IOException {
+    sharedRefDbRefUpdate = getSharedRefDbBatchRefUpdateWithMockedValidator();
+    doThrow(new IOException("IO Test Exception"))
+        .when(batchRefUpdateValidator)
+        .executeBatchUpdateWithValidation(any(), any());
+
+    sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+  }
+
+  @Test
+  public void executeSuccessfullyWithNoExceptionsWhenOutOfSync() throws IOException {
+    setMockRequiredReturnValues();
+    doReturn(true).when(sharedRefDb).exists(A_TEST_PROJECT_NAME_KEY, A_TEST_REF_NAME);
+    doReturn(false).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, oldRef);
+
+    sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+
+    verify(validationMetrics).incrementSplitBrainPrevention();
+  }
+
+  @Test
+  public void executeSuccessfullyWithNoExceptionsWhenEmptyList() throws IOException {
+    doReturn(batchRefUpdate).when(refDatabase).newBatchUpdate();
+    doReturn(Collections.emptyList()).when(batchRefUpdate).getCommands();
+
+    sharedRefDbRefUpdate = getSharedRefDbBatchRefUpdateWithDefaultPolicyEnforcement();
+
+    sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+  }
+
+  private SharedRefDbBatchRefUpdate getSharedRefDbBatchRefUpdateWithDefaultPolicyEnforcement() {
+    BatchRefUpdateValidator.Factory batchRefValidatorFactory =
+        new BatchRefUpdateValidator.Factory() {
+          @Override
+          public BatchRefUpdateValidator create(String projectName, RefDatabase refDb) {
+            return new BatchRefUpdateValidator(
+                sharedRefDb,
+                validationMetrics,
+                new DefaultSharedRefEnforcement(),
+                new DummyLockWrapper(),
+                projectsFilter,
+                projectName,
+                refDb);
+          }
+        };
+    return new SharedRefDbBatchRefUpdate(
+        batchRefValidatorFactory, A_TEST_PROJECT_NAME, refDatabase);
+  }
+
+  private SharedRefDbBatchRefUpdate getSharedRefDbBatchRefUpdateWithMockedValidator() {
+    BatchRefUpdateValidator.Factory batchRefValidatorFactory =
+        new BatchRefUpdateValidator.Factory() {
+          @Override
+          public BatchRefUpdateValidator create(String projectName, RefDatabase refDb) {
+            return batchRefUpdateValidator;
+          }
+        };
+    return new SharedRefDbBatchRefUpdate(
+        batchRefValidatorFactory, A_TEST_PROJECT_NAME, refDatabase);
+  }
+
+  protected static class RefMatcher implements ArgumentMatcher<Ref> {
+    private Ref left;
+
+    public RefMatcher(Ref ref) {
+      this.left = ref;
+    }
+
+    @Override
+    public boolean matches(Ref right) {
+      return left.getName().equals(right.getName())
+          && left.getObjectId().equals(right.getObjectId());
+    }
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManagerTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManagerTest.java
new file mode 100644
index 0000000..b2b8ad6
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManagerTest.java
@@ -0,0 +1,82 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import org.eclipse.jgit.lib.Repository;
+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 SharedRefDbGitRepositoryManagerTest implements RefFixture {
+
+  @Mock LocalDiskRepositoryManager localDiskRepositoryManagerMock;
+
+  @Mock SharedRefDbRepository.Factory sharedRefDbRepositoryFactoryMock;
+
+  @Mock Repository repositoryMock;
+
+  @Mock SharedRefDbRepository sharedRefDbRepositoryMock;
+
+  SharedRefDbGitRepositoryManager msRepoMgr;
+
+  @Override
+  public String testBranch() {
+    return "foo";
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    doReturn(sharedRefDbRepositoryMock)
+        .when(sharedRefDbRepositoryFactoryMock)
+        .create(A_TEST_PROJECT_NAME, repositoryMock);
+    msRepoMgr =
+        new SharedRefDbGitRepositoryManager(
+            sharedRefDbRepositoryFactoryMock, localDiskRepositoryManagerMock);
+  }
+
+  @Test
+  public void openRepositoryShouldCreateSharedRefDbRepositoryWrapper() throws Exception {
+    doReturn(repositoryMock)
+        .when(localDiskRepositoryManagerMock)
+        .openRepository(A_TEST_PROJECT_NAME_KEY);
+
+    msRepoMgr.openRepository(A_TEST_PROJECT_NAME_KEY);
+
+    verifyThatSharedRefDbRepositoryWrapperHasBeenCreated();
+  }
+
+  @Test
+  public void createRepositoryShouldCreateSharedRefDbRepositoryWrapper() throws Exception {
+    doReturn(repositoryMock)
+        .when(localDiskRepositoryManagerMock)
+        .createRepository(A_TEST_PROJECT_NAME_KEY);
+
+    msRepoMgr.createRepository(A_TEST_PROJECT_NAME_KEY);
+
+    verifyThatSharedRefDbRepositoryWrapperHasBeenCreated();
+  }
+
+  private void verifyThatSharedRefDbRepositoryWrapperHasBeenCreated() {
+    verify(sharedRefDbRepositoryFactoryMock).create(A_TEST_PROJECT_NAME, repositoryMock);
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabaseTest.java
new file mode 100644
index 0000000..352b136
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabaseTest.java
@@ -0,0 +1,59 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SharedRefDbRefDatabaseTest implements RefFixture {
+
+  @Rule public TestName nameRule = new TestName();
+
+  @Mock SharedRefDbRefUpdate.Factory refUpdateFactoryMock;
+  @Mock SharedRefDbBatchRefUpdate.Factory refBatchUpdateFactoryMock;
+
+  @Mock RefDatabase refDatabaseMock;
+
+  @Mock RefUpdate refUpdateMock;
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  @Test
+  public void newUpdateShouldCreateSharedRefDbRefUpdate() throws Exception {
+    String refName = aBranchRef();
+    SharedRefDbRefDatabase sharedRefDbRefDb =
+        new SharedRefDbRefDatabase(
+            refUpdateFactoryMock, refBatchUpdateFactoryMock, A_TEST_PROJECT_NAME, refDatabaseMock);
+    doReturn(refUpdateMock).when(refDatabaseMock).newUpdate(refName, false);
+
+    sharedRefDbRefDb.newUpdate(refName, false);
+
+    verify(refUpdateFactoryMock).create(A_TEST_PROJECT_NAME, refUpdateMock, refDatabaseMock);
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepositoryTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepositoryTest.java
new file mode 100644
index 0000000..5bb4fb4
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepositoryTest.java
@@ -0,0 +1,97 @@
+// 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.gerritforge.gerrit.globalrefdb.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture;
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SharedRefDbRepositoryTest implements RefFixture {
+
+  @Mock SharedRefDbRefDatabase.Factory sharedRefDbRefDbFactory;
+  @Mock SharedRefDbRefDatabase sharedRefDb;
+  @Mock RefDatabase genericRefDb;
+  @Mock ObjectDatabase objectDatabase;
+
+  @Mock SharedRefDbRefUpdate sharedRefDbRefUpdate;
+
+  @Mock Repository repository;
+
+  private final String PROJECT_NAME = "ProjectName";
+  private final String REFS_HEADS_MASTER = "refs/heads/master";
+
+  @Override
+  public String testBranch() {
+    return null;
+  }
+
+  private void setMockitoCommon() {
+    doReturn(true).when(repository).isBare();
+    doReturn(genericRefDb).when(repository).getRefDatabase();
+    doReturn(objectDatabase).when(repository).getObjectDatabase();
+    doReturn(sharedRefDb).when(sharedRefDbRefDbFactory).create(PROJECT_NAME, genericRefDb);
+  }
+
+  @Test
+  public void shouldInvokeSharedRefDbRefDbFactoryCreate() {
+    setMockitoCommon();
+    try (SharedRefDbRepository sharedRefDbRepository =
+        new SharedRefDbRepository(sharedRefDbRefDbFactory, PROJECT_NAME, repository)) {
+
+      sharedRefDbRepository.getRefDatabase();
+      verify(sharedRefDbRefDbFactory).create(PROJECT_NAME, genericRefDb);
+    }
+  }
+
+  @Test
+  public void shouldInvokeNewUpdateInSharedRefDbRefDatabase() throws IOException {
+    setMockitoCommon();
+    try (SharedRefDbRepository sharedRefDbRepository =
+        new SharedRefDbRepository(sharedRefDbRefDbFactory, PROJECT_NAME, repository)) {
+      sharedRefDbRepository.getRefDatabase().newUpdate(REFS_HEADS_MASTER, false);
+
+      verify(sharedRefDb).newUpdate(REFS_HEADS_MASTER, false);
+    }
+  }
+
+  @Test
+  public void shouldInvokeUpdateInSharedRefDbRefUpdate() throws IOException {
+    setMockitoCommon();
+    doReturn(Result.NEW).when(sharedRefDbRefUpdate).update();
+    doReturn(sharedRefDbRefUpdate).when(sharedRefDb).newUpdate(REFS_HEADS_MASTER, false);
+
+    try (SharedRefDbRepository sharedRefDbRepository =
+        new SharedRefDbRepository(sharedRefDbRefDbFactory, PROJECT_NAME, repository)) {
+
+      Result updateResult =
+          sharedRefDbRepository.getRefDatabase().newUpdate(REFS_HEADS_MASTER, false).update();
+
+      verify(sharedRefDbRefUpdate).update();
+      assertThat(updateResult).isEqualTo(Result.NEW);
+    }
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProjectTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProjectTest.java
new file mode 100644
index 0000000..4cf6800
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProjectTest.java
@@ -0,0 +1,150 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration;
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration.SharedRefDatabase;
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
+import java.util.Arrays;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CustomSharedRefEnforcementByProjectTest implements RefFixture {
+
+  SharedRefEnforcement refEnforcement;
+
+  @Before
+  public void setUp() {
+    Config sharedRefDbConfig = new Config();
+    sharedRefDbConfig.setStringList(
+        SharedRefDatabase.SECTION,
+        SharedRefDatabase.SUBSECTION_ENFORCEMENT_RULES,
+        EnforcePolicy.IGNORED.name(),
+        Arrays.asList(
+            "ProjectOne",
+            "ProjectTwo:refs/heads/master/test",
+            "ProjectTwo:refs/heads/master/test2"));
+
+    refEnforcement = newCustomRefEnforcement(sharedRefDbConfig);
+  }
+
+  @Test
+  public void projectOneShouldReturnDesiredForAllRefs() {
+    Ref aRef = newRef("refs/heads/master/2", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy("ProjectOne", aRef.getName()))
+        .isEqualTo(EnforcePolicy.IGNORED);
+  }
+
+  @Test
+  public void projectOneEnforcementShouldAlwaysPrevail() {
+    Ref aRef = newRef("refs/heads/master/test", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy("ProjectOne", aRef.getName()))
+        .isEqualTo(EnforcePolicy.IGNORED);
+  }
+
+  @Test
+  public void aNonListedProjectShouldRequireRefForMasterTest() {
+    Ref aRef = newRef("refs/heads/master/test", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy("NonListedProject", aRef.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void projectTwoSpecificRefShouldReturnIgnoredPolicy() {
+    Ref refOne = newRef("refs/heads/master/test", AN_OBJECT_ID_1);
+    Ref refTwo = newRef("refs/heads/master/test2", AN_OBJECT_ID_1);
+
+    assertThat(refEnforcement.getPolicy("ProjectTwo", refOne.getName()))
+        .isEqualTo(EnforcePolicy.IGNORED);
+    assertThat(refEnforcement.getPolicy("ProjectTwo", refTwo.getName()))
+        .isEqualTo(EnforcePolicy.IGNORED);
+  }
+
+  @Test
+  public void aNonListedProjectShouldReturnRequired() {
+    Ref refOne = newRef("refs/heads/master/newChange", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy("NonListedProject", refOne.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void aNonListedRefInProjectShouldReturnRequired() {
+    Ref refOne = newRef("refs/heads/master/test3", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy("ProjectTwo", refOne.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void aNonListedProjectAndRefShouldReturnRequired() {
+    Ref refOne = newRef("refs/heads/master/test3", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy("NonListedProject", refOne.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void getProjectPolicyForProjectOneShouldReturnIgnored() {
+    assertThat(refEnforcement.getPolicy("ProjectOne")).isEqualTo(EnforcePolicy.IGNORED);
+  }
+
+  @Test
+  public void getProjectPolicyForProjectTwoShouldReturnRequired() {
+    assertThat(refEnforcement.getPolicy("ProjectTwo")).isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void getProjectPolicyForNonListedProjectShouldReturnRequired() {
+    assertThat(refEnforcement.getPolicy("NonListedProject")).isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void getProjectPolicyForNonListedProjectWhenSingleProject() {
+    SharedRefEnforcement customEnforcement =
+        newCustomRefEnforcementWithValue(EnforcePolicy.IGNORED, ":refs/heads/master");
+
+    assertThat(customEnforcement.getPolicy("NonListedProject")).isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void getANonListedProjectWhenOnlyOneProjectIsListedShouldReturnRequired() {
+    SharedRefEnforcement customEnforcement =
+        newCustomRefEnforcementWithValue(EnforcePolicy.IGNORED, "AProject:");
+    assertThat(customEnforcement.getPolicy("NonListedProject", "refs/heads/master"))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  private SharedRefEnforcement newCustomRefEnforcementWithValue(
+      EnforcePolicy policy, String... projectAndRefs) {
+    Config sharedRefDbConfiguration = new Config();
+    sharedRefDbConfiguration.setStringList(
+        SharedRefDatabase.SECTION,
+        SharedRefDatabase.SUBSECTION_ENFORCEMENT_RULES,
+        policy.name(),
+        Arrays.asList(projectAndRefs));
+    return newCustomRefEnforcement(sharedRefDbConfiguration);
+  }
+
+  private SharedRefEnforcement newCustomRefEnforcement(Config sharedRefDbConfig) {
+    return new CustomSharedRefEnforcementByProject(new SharedRefDbConfiguration(sharedRefDbConfig));
+  }
+
+  @Override
+  public String testBranch() {
+    return "fooBranch";
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcementTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcementTest.java
new file mode 100644
index 0000000..a9f0cb2
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcementTest.java
@@ -0,0 +1,80 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Test;
+
+public class DefaultSharedRefEnforcementTest implements RefFixture {
+
+  SharedRefEnforcement refEnforcement = new DefaultSharedRefEnforcement();
+
+  @Test
+  public void anImmutableChangeShouldBeIgnored() {
+    Ref immutableChangeRef = newRef(A_REF_NAME_OF_A_PATCHSET, AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName()))
+        .isEqualTo(EnforcePolicy.IGNORED);
+  }
+
+  @Test
+  public void aChangeMetaShouldNotBeIgnored() {
+    Ref immutableChangeRef = newRef("refs/changes/01/1/meta", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void aCacheAutomergeShouldBeIgnored() {
+    Ref immutableChangeRef = newRef("refs/cache-automerge/01/1/1000000", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName()))
+        .isEqualTo(EnforcePolicy.IGNORED);
+  }
+
+  @Test
+  public void aDraftCommentsShouldBeIgnored() {
+    Ref immutableChangeRef = newRef("refs/draft-comments/01/1/1000000", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName()))
+        .isEqualTo(EnforcePolicy.IGNORED);
+  }
+
+  @Test
+  public void regularRefHeadsMasterShouldNotBeIgnored() {
+    Ref immutableChangeRef = newRef("refs/heads/master", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void regularCommitShouldNotBeIgnored() {
+    Ref immutableChangeRef = newRef("refs/heads/stable-2.16", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Test
+  public void allUsersExternalIdsRefShouldBeRequired() {
+    Ref refOne = newRef("refs/meta/external-ids", AN_OBJECT_ID_1);
+    assertThat(refEnforcement.getPolicy("All-Users", refOne.getName()))
+        .isEqualTo(EnforcePolicy.REQUIRED);
+  }
+
+  @Override
+  public String testBranch() {
+    return "fooBranch";
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java
new file mode 100644
index 0000000..bc1435c
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java
@@ -0,0 +1,42 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Test;
+
+public class NoopSharedRefDatabaseTest implements RefFixture {
+
+  private Ref sampleRef = newRef(A_TEST_REF_NAME, AN_OBJECT_ID_1);
+  private NoopSharedRefDatabase objectUnderTest = new NoopSharedRefDatabase();
+
+  @Test
+  public void isUpToDateShouldAlwaysReturnTrue() {
+    assertThat(objectUnderTest.isUpToDate(A_TEST_PROJECT_NAME_KEY, sampleRef)).isTrue();
+  }
+
+  @Test
+  public void compareAndPutShouldAlwaysReturnTrue() {
+    assertThat(objectUnderTest.compareAndPut(A_TEST_PROJECT_NAME_KEY, sampleRef, AN_OBJECT_ID_2))
+        .isTrue();
+  }
+
+  @Test
+  public void existsShouldAlwaysReturnFalse() {
+    assertThat(objectUnderTest.exists(A_TEST_PROJECT_NAME_KEY, A_TEST_REF_NAME)).isFalse();
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefFixture.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefFixture.java
new file mode 100644
index 0000000..fa6a825
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefFixture.java
@@ -0,0 +1,54 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Ignore;
+
+@Ignore
+public interface RefFixture {
+
+  static final String ALLOWED_CHARS = "abcdefghilmnopqrstuvz";
+  static final String ALLOWED_DIGITS = "1234567890";
+  static final String ALLOWED_NAME_CHARS =
+      ALLOWED_CHARS + ALLOWED_CHARS.toUpperCase() + ALLOWED_DIGITS;
+  static final String A_TEST_PROJECT_NAME = "A_TEST_PROJECT_NAME";
+  static final Project.NameKey A_TEST_PROJECT_NAME_KEY = Project.nameKey(A_TEST_PROJECT_NAME);
+  static final ObjectId AN_OBJECT_ID_1 = new ObjectId(1, 2, 3, 4, 5);
+  static final ObjectId AN_OBJECT_ID_2 = new ObjectId(1, 2, 3, 4, 6);
+  static final ObjectId AN_OBJECT_ID_3 = new ObjectId(1, 2, 3, 4, 7);
+  static final String A_TEST_REF_NAME = "refs/heads/master";
+  static final String A_REF_NAME_OF_A_PATCHSET = "refs/changes/01/1/1";
+
+  default String aBranchRef() {
+    return RefNames.REFS_HEADS + testBranch();
+  }
+
+  default String testBranch() {
+    return "aTestBranch";
+  }
+
+  default Ref newRef(String refName, ObjectId objectId) {
+    return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, refName, objectId);
+  }
+
+  default Ref nullRef(String refName) {
+    return newRef(refName, ObjectId.zeroId());
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefSharedDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefSharedDatabaseTest.java
new file mode 100644
index 0000000..2d3d8b8
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefSharedDatabaseTest.java
@@ -0,0 +1,47 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Ref.Storage;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class RefSharedDatabaseTest implements RefFixture {
+  @Rule public TestName nameRule = new TestName();
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  @Test
+  public void shouldCreateANewRef() {
+
+    ObjectId objectId = AN_OBJECT_ID_1;
+    String refName = aBranchRef();
+
+    Ref aNewRef = new ObjectIdRef.Unpeeled(Storage.NETWORK, refName, objectId);
+
+    assertThat(aNewRef.getName()).isEqualTo(refName);
+    assertThat(aNewRef.getObjectId()).isEqualTo(objectId);
+    assertThat(aNewRef.getStorage()).isEqualTo(Storage.NETWORK);
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefUpdateStub.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefUpdateStub.java
new file mode 100644
index 0000000..e78ba6d
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefUpdateStub.java
@@ -0,0 +1,114 @@
+// 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
+
+import java.io.IOException;
+import org.apache.commons.lang.NotImplementedException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Ignore;
+
+@Ignore
+public class RefUpdateStub extends RefUpdate {
+
+  public static RefUpdate forSuccessfulCreate(Ref newRef) {
+    return new RefUpdateStub(Result.NEW, null, newRef, newRef.getObjectId());
+  }
+
+  public static RefUpdate forSuccessfulUpdate(Ref oldRef, ObjectId newObjectId) {
+    return new RefUpdateStub(Result.FAST_FORWARD, null, oldRef, newObjectId);
+  }
+
+  public static RefUpdate forSuccessfulDelete(Ref oldRef) {
+    return new RefUpdateStub(null, Result.FORCED, oldRef, ObjectId.zeroId());
+  }
+
+  private final Result updateResult;
+  private final Result deleteResult;
+
+  public RefUpdateStub(Result updateResult, Result deleteResult, Ref oldRef, ObjectId newObjectId) {
+    super(oldRef);
+    this.setNewObjectId(newObjectId);
+    this.updateResult = updateResult;
+    this.deleteResult = deleteResult;
+  }
+
+  @Override
+  protected RefDatabase getRefDatabase() {
+    throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!");
+  }
+
+  @Override
+  protected Repository getRepository() {
+    throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!");
+  }
+
+  @Override
+  protected boolean tryLock(boolean deref) throws IOException {
+    throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!");
+  }
+
+  @Override
+  protected void unlock() {
+    throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!");
+  }
+
+  @Override
+  protected Result doUpdate(Result desiredResult) throws IOException {
+    throw new NotImplementedException("Method not implemented, shouldn't be called!!");
+  }
+
+  @Override
+  protected Result doDelete(Result desiredResult) throws IOException {
+    throw new NotImplementedException("Method not implemented, shouldn't be called!!");
+  }
+
+  @Override
+  protected Result doLink(String target) throws IOException {
+    throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!");
+  }
+
+  @Override
+  public Result update() throws IOException {
+    if (updateResult != null) return updateResult;
+
+    throw new NotImplementedException("Not assumed you needed to stub this call!!");
+  }
+
+  @Override
+  public Result update(RevWalk walk) throws IOException {
+    if (updateResult != null) return updateResult;
+
+    throw new NotImplementedException("Not assumed you needed to stub this call!!");
+  }
+
+  @Override
+  public Result delete() throws IOException {
+    if (deleteResult != null) return deleteResult;
+
+    throw new NotImplementedException("Not assumed you needed to stub this call!!");
+  }
+
+  @Override
+  public Result delete(RevWalk walk) throws IOException {
+    if (deleteResult != null) return deleteResult;
+
+    throw new NotImplementedException("Not assumed you needed to stub this call!!");
+  }
+}