blob: 2c00acd5f7744a47060f39fe35c988cd1473dad3 [file] [log] [blame]
// Copyright (C) 2013 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.google.gerrit.testing;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.ACCOUNTS_UPDATE;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BAN_COMMIT;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.DIRECT_PUSH;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GPG_KEYS_MODIFICATION;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GROUPS_UPDATE;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.HEAD_MODIFICATION;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.MERGE_CHANGE;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.OFFLINE_OPERATION;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.REPO_SEQ;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.TAG_MODIFICATION;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.VERSIONED_META_DATA_CHANGE;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.RepositoryCaseMismatchException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
import com.google.inject.Inject;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.function.Predicate;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase;
import org.eclipse.jgit.internal.storage.dfs.DfsReftableBatchRefUpdate;
import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
/** Repository manager that uses in-memory repositories. */
public class InMemoryRepositoryManager implements GitRepositoryManager {
public static InMemoryRepository newRepository(Project.NameKey name) {
return new Repo(name);
}
public static class Description extends DfsRepositoryDescription {
private final Project.NameKey name;
private Description(Project.NameKey name) {
super(name.get());
this.name = name;
}
public Project.NameKey getProject() {
return name;
}
}
public static class Repo extends InMemoryRepository {
private String description;
private Repo(Project.NameKey name) {
super(new Description(name));
setPerformsAtomicTransactions(true);
}
/** Validates that a given ref is updated within the expected context. */
private static class RefUpdateContextValidator {
/**
* A configured singleton for ref context validation.
*
* <p>Each ref must match no more than 1 special ref from the list below. If ref is not
* matched to any special ref predicate, then it is checked against the standard rules - check
* the code of the {@link #validateRefUpdateContext} for details.
*/
public static final RefUpdateContextValidator INSTANCE =
new RefUpdateContextValidator()
.addSpecialRef(RefNames::isSequenceRef, REPO_SEQ)
.addSpecialRef(RefNames.HEAD::equals, HEAD_MODIFICATION)
.addSpecialRef(RefNames::isRefsChanges, CHANGE_MODIFICATION, MERGE_CHANGE)
.addSpecialRef(RefNames::isAutoMergeRef, CHANGE_MODIFICATION, MERGE_CHANGE)
.addSpecialRef(RefNames::isRefsEdit, CHANGE_MODIFICATION, MERGE_CHANGE)
.addSpecialRef(RefNames::isTagRef, TAG_MODIFICATION)
.addSpecialRef(RefNames::isRejectCommitsRef, BAN_COMMIT)
.addSpecialRef(
name -> RefNames.isRefsUsers(name) && !RefNames.isRefsEdit(name),
VERSIONED_META_DATA_CHANGE,
ACCOUNTS_UPDATE,
MERGE_CHANGE)
.addSpecialRef(
RefNames::isConfigRef,
VERSIONED_META_DATA_CHANGE,
BRANCH_MODIFICATION,
MERGE_CHANGE)
.addSpecialRef(RefNames::isExternalIdRef, VERSIONED_META_DATA_CHANGE, ACCOUNTS_UPDATE)
.addSpecialRef(PublicKeyStore.REFS_GPG_KEYS::equals, GPG_KEYS_MODIFICATION)
.addSpecialRef(RefNames::isRefsDraftsComments, CHANGE_MODIFICATION)
.addSpecialRef(RefNames::isRefsStarredChanges, CHANGE_MODIFICATION)
// A user can create a change for updating a group and then merge it.
// The GroupsIT#pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit test verifies
// this scenario.
.addSpecialRef(RefNames::isGroupRef, GROUPS_UPDATE, MERGE_CHANGE);
private List<Entry<Predicate<String>, ImmutableList<RefUpdateType>>> specialRefs =
new ArrayList<>();
private RefUpdateContextValidator() {}
public void validateRefUpdateContext(ReceiveCommand cmd) {
String refName = cmd.getRefName();
if (RefUpdateContextCollector.enabled()) {
RefUpdateContextCollector.register(refName, RefUpdateContext.getOpenedContexts());
}
if (TestActionRefUpdateContext.isOpen()
|| RefUpdateContext.hasOpen(OFFLINE_OPERATION)
|| RefUpdateContext.hasOpen(INIT_REPO)
|| RefUpdateContext.hasOpen(DIRECT_PUSH)) {
// The action can touch any refs in these contexts.
return;
}
Optional<ImmutableList<RefUpdateType>> allowedRefUpdateTypes =
RefUpdateContextValidator.INSTANCE.getAllowedRefUpdateTypes(refName);
if (allowedRefUpdateTypes.isPresent()) {
checkState(
allowedRefUpdateTypes.get().stream().anyMatch(RefUpdateContext::hasOpen)
|| isTestRepoCall(),
"Special ref '%s' is updated outside of the expected operation. Wrap code in the correct RefUpdateContext or fix allowed update types",
refName);
return;
}
// It is not one of the special ref - update is possible only within specific contexts.
checkState(
RefUpdateContext.hasOpen(MERGE_CHANGE)
|| RefUpdateContext.hasOpen(RefUpdateType.BRANCH_MODIFICATION)
|| RefUpdateContext.hasOpen(RefUpdateType.UPDATE_SUPERPROJECT)
// Plugin can update any ref
|| RefUpdateContext.hasOpen(PLUGIN)
|| isTestRepoCall(),
"Ordinary ref '%s' is updated outside of the expected operation. Wrap code in the correct RefUpdateContext or add the ref as a special ref.",
refName);
}
private RefUpdateContextValidator addSpecialRef(
Predicate<String> refNamePredicate, RefUpdateType... validRefUpdateTypes) {
specialRefs.add(
new SimpleImmutableEntry<>(
refNamePredicate, ImmutableList.copyOf(validRefUpdateTypes)));
return this;
}
private Optional<ImmutableList<RefUpdateType>> getAllowedRefUpdateTypes(String refName) {
List<ImmutableList<RefUpdateType>> allowedTypes =
specialRefs.stream()
.filter(entry -> entry.getKey().test(refName))
.map(Entry::getValue)
.collect(toList());
checkState(
allowedTypes.size() <= 1,
"refName matches more than 1 predicate. Please fix the specialRefs list, so each reference has no more than one match.");
if (allowedTypes.size() == 0) {
return Optional.empty();
}
return Optional.of(allowedTypes.get(0));
}
/**
* Returns true if a ref is updated using one of the method in {@link
* org.eclipse.jgit.junit.TestRepository}.
*
* <p>The {@link org.eclipse.jgit.junit.TestRepository} used only in tests and allows to
* change refs directly. Wrapping each usage in a test context requires a lot of modification,
* so instead we allow any ref updates, which are made using through this class.
*/
private boolean isTestRepoCall() {
return Arrays.stream(Thread.currentThread().getStackTrace())
.anyMatch(elem -> elem.getClassName().equals("org.eclipse.jgit.junit.TestRepository"));
}
}
@Override
protected MemRefDatabase createRefDatabase() {
return new MemRefDatabase() {
@Override
public BatchRefUpdate newBatchUpdate() {
DfsObjDatabase odb = getRepository().getObjectDatabase();
return new DfsReftableBatchRefUpdate(this, odb) {
@Override
public void execute(RevWalk rw, ProgressMonitor pm, List<String> options) {
getCommands().stream()
.forEach(RefUpdateContextValidator.INSTANCE::validateRefUpdateContext);
super.execute(rw, pm, options);
}
};
}
};
}
@Override
public Description getDescription() {
return (Description) super.getDescription();
}
@Override
public String getGitwebDescription() {
return description;
}
@Override
public void setGitwebDescription(String d) {
description = d;
}
}
private final Map<String, Repo> repos;
@Inject
public InMemoryRepositoryManager() {
this.repos = new HashMap<>();
}
@Override
public synchronized Status getRepositoryStatus(NameKey name) {
try {
@SuppressWarnings("unused")
var unused = get(name);
return Status.ACTIVE;
} catch (RepositoryNotFoundException e) {
return Status.NON_EXISTENT;
}
}
@Override
public synchronized Repo openRepository(Project.NameKey name) throws RepositoryNotFoundException {
return get(name);
}
@Override
public synchronized Repo createRepository(Project.NameKey name)
throws RepositoryCaseMismatchException, RepositoryNotFoundException {
Repo repo;
try {
repo = get(name);
if (!repo.getDescription().getRepositoryName().equals(name.get())) {
throw new RepositoryCaseMismatchException(name);
}
} catch (RepositoryNotFoundException e) {
repo = new Repo(name);
repos.put(normalize(name), repo);
}
return repo;
}
@Override
public synchronized NavigableSet<Project.NameKey> list() {
NavigableSet<Project.NameKey> names = Sets.newTreeSet();
for (DfsRepository repo : repos.values()) {
names.add(Project.nameKey(repo.getDescription().getRepositoryName()));
}
return Collections.unmodifiableNavigableSet(names);
}
public synchronized void deleteRepository(Project.NameKey name) {
repos.remove(normalize(name));
}
private synchronized Repo get(Project.NameKey name) throws RepositoryNotFoundException {
Repo repo = repos.get(normalize(name));
if (repo != null) {
repo.incrementOpen();
return repo;
}
throw new RepositoryNotFoundException(name.get());
}
private static String normalize(Project.NameKey name) {
return name.get().toLowerCase(Locale.US);
}
}