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!!");
+ }
+}