All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.acceptance.ProjectResetter Maven / Gradle / Ivy

There is a newer version: 3.11.0
Show newest version
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.acceptance;

import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.entities.RefNames.REFS_USERS;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static java.util.stream.Collectors.toSet;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.index.RefState;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.RefPatternMatcher;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;

/**
 * Saves the states of given projects and resets the project states on close.
 *
 * 

Saving the project states is done by saving the states of all refs in the project. On close * those refs are reset to the saved states. Refs that were newly created are deleted. * *

By providing ref patterns per project it can be controlled which refs should be reset on * close. * *

If resetting touches {@code refs/meta/config} branches the corresponding projects are evicted * from the project cache. * *

If resetting touches user branches or the {@code refs/meta/external-ids} branch the * corresponding accounts are evicted from the account cache and also if needed from the cache in * {@link AccountCreator}. * *

At the moment this class has the following limitations: * *

    *
  • Resetting group branches doesn't evict the corresponding groups from the group cache. *
  • Changes are not reindexed if change meta refs are reset. *
  • Changes are not reindexed if starred-changes refs in All-Users are reset. *
  • If accounts are deleted changes may still refer to these accounts (e.g. as reviewers). *
* * Primarily this class is intended to reset the states of the All-Projects and All-Users projects * after each test. These projects rarely contain changes and it's currently not a problem if these * changes get stale. For creating changes each test gets a brand new project. Since this project is * not used outside of the test method that creates it, it doesn't need to be reset. */ public class ProjectResetter implements AutoCloseable { public static class Builder { public interface Factory { Builder builder(); } private final GitRepositoryManager repoManager; private final AllUsersName allUsersName; @Nullable private final AccountCreator accountCreator; @Nullable private final AccountCache accountCache; @Nullable private final AccountIndexer accountIndexer; @Nullable private final GroupCache groupCache; @Nullable private final GroupIncludeCache groupIncludeCache; @Nullable private final GroupIndexer groupIndexer; @Nullable private final ProjectCache projectCache; @Inject public Builder( GitRepositoryManager repoManager, AllUsersName allUsersName, @Nullable AccountCreator accountCreator, @Nullable AccountCache accountCache, @Nullable AccountIndexer accountIndexer, @Nullable GroupCache groupCache, @Nullable GroupIncludeCache groupIncludeCache, @Nullable GroupIndexer groupIndexer, @Nullable ProjectCache projectCache) { this.repoManager = repoManager; this.allUsersName = allUsersName; this.accountCreator = accountCreator; this.accountCache = accountCache; this.accountIndexer = accountIndexer; this.groupCache = groupCache; this.groupIncludeCache = groupIncludeCache; this.groupIndexer = groupIndexer; this.projectCache = projectCache; } public ProjectResetter build(ProjectResetter.Config input) throws IOException { return new ProjectResetter( repoManager, allUsersName, accountCreator, accountCache, accountIndexer, groupCache, groupIncludeCache, groupIndexer, projectCache, input.refsByProject); } } public static class Config { private final ImmutableMultimap refsByProject; private Config(ImmutableMultimap refsByProject) { this.refsByProject = refsByProject; } public Builder toBuilder() { Builder builder = new Builder(); builder.refsByProject.putAll(refsByProject); return builder; } public static class Builder { private final ListMultimap refsByProject; public Builder() { this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); } public Builder reset(Project.NameKey project, String... refPatterns) { List refPatternList = Arrays.asList(refPatterns); if (refPatternList.isEmpty()) { refPatternList = ImmutableList.of(RefNames.REFS + "*"); } refsByProject.putAll(project, refPatternList); return this; } public Config build() { return new Config(ImmutableMultimap.copyOf(refsByProject)); } } } @Inject private GitRepositoryManager repoManager; @Inject private AllUsersName allUsersName; @Inject @Nullable private AccountCreator accountCreator; @Inject @Nullable private AccountCache accountCache; @Inject @Nullable private GroupCache groupCache; @Inject @Nullable private GroupIncludeCache groupIncludeCache; @Inject @Nullable private GroupIndexer groupIndexer; @Inject @Nullable private AccountIndexer accountIndexer; @Inject @Nullable private ProjectCache projectCache; private final Multimap refsPatternByProject; // State to which to reset to. private final ListMultimap savedRefStatesByProject; // Results of the resetting private ListMultimap keptRefsByProject; private ListMultimap restoredRefsByProject; private ListMultimap deletedRefsByProject; private ProjectResetter( GitRepositoryManager repoManager, AllUsersName allUsersName, @Nullable AccountCreator accountCreator, @Nullable AccountCache accountCache, @Nullable AccountIndexer accountIndexer, @Nullable GroupCache groupCache, @Nullable GroupIncludeCache groupIncludeCache, @Nullable GroupIndexer groupIndexer, @Nullable ProjectCache projectCache, Multimap refPatternByProject) throws IOException { this.repoManager = repoManager; this.allUsersName = allUsersName; this.accountCreator = accountCreator; this.accountCache = accountCache; this.accountIndexer = accountIndexer; this.groupCache = groupCache; this.groupIndexer = groupIndexer; this.groupIncludeCache = groupIncludeCache; this.projectCache = projectCache; this.refsPatternByProject = refPatternByProject; this.savedRefStatesByProject = readRefStates(); } @Override public void close() throws Exception { keptRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); restoredRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); deletedRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); testRefAction( () -> { restoreRefs(); deleteNewlyCreatedRefs(); evictCachesAndReindex(); }); } /** Read the states of all matching refs. */ private ListMultimap readRefStates() throws IOException { ListMultimap refStatesByProject = MultimapBuilder.hashKeys().arrayListValues().build(); for (Map.Entry> e : refsPatternByProject.asMap().entrySet()) { try (Repository repo = repoManager.openRepository(e.getKey())) { List refs = repo.getRefDatabase().getRefs(); for (String refPattern : e.getValue()) { RefPatternMatcher matcher = RefPatternMatcher.getMatcher(refPattern); for (Ref ref : refs) { if (matcher.match(ref.getName(), null)) { refStatesByProject.put(e.getKey(), RefState.create(ref.getName(), ref.getObjectId())); } } } } } return refStatesByProject; } private void restoreRefs() throws IOException { for (Map.Entry> e : savedRefStatesByProject.asMap().entrySet()) { try (Repository repo = repoManager.openRepository(e.getKey())) { for (RefState refState : e.getValue()) { if (refState.match(repo)) { keptRefsByProject.put(e.getKey(), refState.ref()); continue; } Ref ref = repo.exactRef(refState.ref()); RefUpdate updateRef = repo.updateRef(refState.ref()); updateRef.setExpectedOldObjectId(ref != null ? ref.getObjectId() : ObjectId.zeroId()); updateRef.setNewObjectId(refState.id()); updateRef.setForceUpdate(true); RefUpdate.Result result = updateRef.update(); checkState( result == RefUpdate.Result.FORCED || result == RefUpdate.Result.NEW, "resetting branch %s in %s failed", refState.ref(), e.getKey()); restoredRefsByProject.put(e.getKey(), refState.ref()); } } } } private void deleteNewlyCreatedRefs() throws IOException { for (Map.Entry> e : refsPatternByProject.asMap().entrySet()) { try (Repository repo = repoManager.openRepository(e.getKey())) { Set nonRestoredRefs = repo.getRefDatabase().getRefs().stream() .filter( r -> !keptRefsByProject.containsEntry(e.getKey(), r.getName()) && !restoredRefsByProject.containsEntry(e.getKey(), r.getName())) .collect(toSet()); for (String refPattern : e.getValue()) { RefPatternMatcher matcher = RefPatternMatcher.getMatcher(refPattern); for (Ref ref : nonRestoredRefs) { if (matcher.match(ref.getName(), null) && !deletedRefsByProject.containsEntry(e.getKey(), ref.getName())) { RefUpdate updateRef = repo.updateRef(ref.getName()); updateRef.setExpectedOldObjectId(ref.getObjectId()); updateRef.setNewObjectId(ObjectId.zeroId()); updateRef.setForceUpdate(true); RefUpdate.Result result = updateRef.delete(); checkState( result == RefUpdate.Result.FORCED, "deleting branch %s in %s failed", ref.getName(), e.getKey()); deletedRefsByProject.put(e.getKey(), ref.getName()); } } } } } } private void evictCachesAndReindex() throws IOException { evictAndReindexProjects(); evictAndReindexAccounts(); evictAndReindexGroups(); // TODO(ekempin): Reindex changes if starred-changes refs in All-Users were modified. } /** Evict projects for which the config was changed. */ private void evictAndReindexProjects() { if (projectCache == null) { return; } for (Project.NameKey project : Sets.union( projectsWithConfigChanges(restoredRefsByProject), projectsWithConfigChanges(deletedRefsByProject))) { projectCache.evictAndReindex(project); } } private Set projectsWithConfigChanges( Multimap projects) { return projects.entries().stream() .filter(e -> e.getValue().equals(RefNames.REFS_CONFIG)) .map(Map.Entry::getKey) .collect(toSet()); } /** Evict accounts that were modified. */ private void evictAndReindexAccounts() throws IOException { Set deletedAccounts = accountIds(deletedRefsByProject.get(allUsersName).stream()); if (accountCreator != null) { accountCreator.evict(deletedAccounts); } if (accountCache != null || accountIndexer != null) { Set modifiedAccounts = new HashSet<>(accountIds(restoredRefsByProject.get(allUsersName).stream())); if (restoredRefsByProject.get(allUsersName).contains(RefNames.REFS_EXTERNAL_IDS) || deletedRefsByProject.get(allUsersName).contains(RefNames.REFS_EXTERNAL_IDS)) { // The external IDs have been modified but we don't know which accounts were affected. // Make sure all accounts are evicted and reindexed. try (Repository repo = repoManager.openRepository(allUsersName)) { for (Account.Id id : accountIds(repo)) { reindexAccount(id); } } // Remove deleted accounts from the cache and index. for (Account.Id id : deletedAccounts) { reindexAccount(id); } } else { // Evict and reindex all modified and deleted accounts. for (Account.Id id : Sets.union(modifiedAccounts, deletedAccounts)) { reindexAccount(id); } } } } /** Evict groups that were modified. */ private void evictAndReindexGroups() { if (groupCache != null || groupIndexer != null) { Set modifiedGroups = new HashSet<>(groupUUIDs(restoredRefsByProject.get(allUsersName))); Set deletedGroups = new HashSet<>(groupUUIDs(deletedRefsByProject.get(allUsersName))); // Evict and reindex all modified and deleted groups. for (AccountGroup.UUID uuid : Sets.union(modifiedGroups, deletedGroups)) { evictAndReindexGroup(uuid); } } } private void reindexAccount(Account.Id accountId) { if (groupIncludeCache != null) { groupIncludeCache.evictGroupsWithMember(accountId); } if (accountIndexer != null) { accountIndexer.index(accountId); } } private void evictAndReindexGroup(AccountGroup.UUID uuid) { if (groupCache != null) { groupCache.evict(uuid); } if (groupIncludeCache != null) { groupIncludeCache.evictParentGroupsOf(uuid); } if (groupIndexer != null) { groupIndexer.index(uuid); } } private static Set accountIds(Repository repo) throws IOException { return accountIds(repo.getRefDatabase().getRefsByPrefix(REFS_USERS).stream().map(Ref::getName)); } private static Set accountIds(Stream refs) { return refs.filter(r -> r.startsWith(REFS_USERS)) .map(Account.Id::fromRef) .filter(Objects::nonNull) .collect(toSet()); } private Set groupUUIDs(Collection refs) { return refs.stream() .filter(RefNames::isRefsGroups) .map(AccountGroup.UUID::fromRef) .filter(Objects::nonNull) .collect(toSet()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy