| // 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.google.gerrit.server.submit; |
| |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSetMultimap; |
| import com.google.common.collect.MultimapBuilder; |
| import com.google.common.collect.SetMultimap; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.UsedAt; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.entities.SubmoduleSubscription; |
| import com.google.gerrit.entities.SubscribeSection; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo; |
| import com.google.inject.AbstractModule; |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Deque; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| |
| /** |
| * A container which stores subscription relationship. A SubscriptionGraph is calculated every time |
| * changes are pushed. Some branches are updated in these changes, and if these branches are |
| * subscribed by other projects, SubscriptionGraph would record information about these updated |
| * branches and branches/projects affected. |
| */ |
| public class SubscriptionGraph { |
| /** Branches updated as part of the enclosing submit or push batch. */ |
| private final ImmutableSet<BranchNameKey> updatedBranches; |
| |
| /** |
| * All branches affected, including those in superprojects and submodules, sorted by submodule |
| * traversal order. To support nested subscriptions, GitLink commits need to be updated in order. |
| * The closer to topological "leaf", the earlier a commit should be updated. |
| * |
| * <p>For example, there are three projects, top level project p1 subscribed to p2, p2 subscribed |
| * to bottom level project p3. When submit a change for p3. We need update both p2 and p1. To be |
| * more precise, we need update p2 first and then update p1. |
| */ |
| private final ImmutableSet<BranchNameKey> sortedBranches; |
| |
| /** Multimap of superproject branch to submodule subscriptions contained in that branch. */ |
| private final ImmutableSetMultimap<BranchNameKey, SubmoduleSubscription> targets; |
| |
| /** |
| * Multimap of superproject name to all branch names within that superproject which have submodule |
| * subscriptions. |
| */ |
| private final ImmutableSetMultimap<Project.NameKey, BranchNameKey> branchesByProject; |
| |
| /** All branches subscribed by other projects. */ |
| private final ImmutableSet<BranchNameKey> subscribedBranches; |
| |
| public SubscriptionGraph( |
| Set<BranchNameKey> updatedBranches, |
| SetMultimap<BranchNameKey, SubmoduleSubscription> targets, |
| SetMultimap<Project.NameKey, BranchNameKey> branchesByProject, |
| Set<BranchNameKey> subscribedBranches, |
| Set<BranchNameKey> sortedBranches) { |
| this.updatedBranches = ImmutableSet.copyOf(updatedBranches); |
| this.targets = ImmutableSetMultimap.copyOf(targets); |
| this.branchesByProject = ImmutableSetMultimap.copyOf(branchesByProject); |
| this.subscribedBranches = ImmutableSet.copyOf(subscribedBranches); |
| this.sortedBranches = ImmutableSet.copyOf(sortedBranches); |
| } |
| |
| /** Returns an empty {@code SubscriptionGraph}. */ |
| static SubscriptionGraph createEmptyGraph(Set<BranchNameKey> updatedBranches) { |
| return new SubscriptionGraph( |
| updatedBranches, |
| ImmutableSetMultimap.of(), |
| ImmutableSetMultimap.of(), |
| ImmutableSet.of(), |
| ImmutableSet.of()); |
| } |
| |
| /** Get branches updated as part of the enclosing submit or push batch. */ |
| public ImmutableSet<BranchNameKey> getUpdatedBranches() { |
| return updatedBranches; |
| } |
| |
| /** Get all superprojects affected. */ |
| public ImmutableSet<Project.NameKey> getAffectedSuperProjects() { |
| return branchesByProject.keySet(); |
| } |
| |
| /** See if a {@code project} is a superproject affected. */ |
| boolean isAffectedSuperProject(Project.NameKey project) { |
| return branchesByProject.containsKey(project); |
| } |
| |
| /** |
| * Returns all branches within the superproject {@code project} which have submodule |
| * subscriptions. |
| */ |
| public ImmutableSet<BranchNameKey> getAffectedSuperBranches(Project.NameKey project) { |
| return branchesByProject.get(project); |
| } |
| |
| /** |
| * Get all affected branches, including the submodule branches and superproject branches, sorted |
| * by traversal order. |
| * |
| * @see SubscriptionGraph#sortedBranches |
| */ |
| public ImmutableSet<BranchNameKey> getSortedSuperprojectAndSubmoduleBranches() { |
| return sortedBranches; |
| } |
| |
| /** Check if a {@code branch} is a submodule of a superproject. */ |
| @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT) |
| public boolean hasSuperproject(BranchNameKey branch) { |
| return subscribedBranches.contains(branch); |
| } |
| |
| /** See if a {@code branch} is a superproject branch affected. */ |
| public boolean hasSubscription(BranchNameKey branch) { |
| return targets.containsKey(branch); |
| } |
| |
| /** Get all related {@code SubmoduleSubscription}s whose super branch is {@code branch}. */ |
| public ImmutableSet<SubmoduleSubscription> getSubscriptions(BranchNameKey branch) { |
| return targets.get(branch); |
| } |
| |
| public interface Factory { |
| SubscriptionGraph compute(Set<BranchNameKey> updatedBranches, MergeOpRepoManager orm) |
| throws SubmoduleConflictException; |
| } |
| |
| public static class SubscriptionGraphModule extends AbstractModule { |
| @Override |
| protected void configure() { |
| bind(Factory.class).annotatedWith(VanillaSubscriptionGraph.class).to(DefaultFactory.class); |
| } |
| } |
| |
| public static class DefaultFactory implements Factory { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| private final ProjectCache projectCache; |
| private final GitModules.Factory gitmodulesFactory; |
| |
| @Inject |
| DefaultFactory(GitModules.Factory gitmodulesFactory, ProjectCache projectCache) { |
| this.gitmodulesFactory = gitmodulesFactory; |
| this.projectCache = projectCache; |
| } |
| |
| @Override |
| public SubscriptionGraph compute(Set<BranchNameKey> updatedBranches, MergeOpRepoManager orm) |
| throws SubmoduleConflictException { |
| Map<BranchNameKey, GitModules> branchGitModules = new HashMap<>(); |
| // All affected branches, including those in superprojects and submodules. |
| Set<BranchNameKey> affectedBranches = new HashSet<>(); |
| |
| // See SubscriptionGraph#targets. |
| SetMultimap<BranchNameKey, SubmoduleSubscription> targets = |
| MultimapBuilder.hashKeys().hashSetValues().build(); |
| |
| // See SubscriptionGraph#branchesByProject. |
| SetMultimap<Project.NameKey, BranchNameKey> branchesByProject = |
| MultimapBuilder.hashKeys().hashSetValues().build(); |
| |
| // See SubscriptionGraph#subscribedBranches. |
| Set<BranchNameKey> subscribedBranches = new HashSet<>(); |
| |
| Set<BranchNameKey> sortedBranches = |
| calculateSubscriptionMaps( |
| updatedBranches, |
| affectedBranches, |
| targets, |
| branchesByProject, |
| subscribedBranches, |
| branchGitModules, |
| orm); |
| |
| return new SubscriptionGraph( |
| updatedBranches, targets, branchesByProject, subscribedBranches, sortedBranches); |
| } |
| |
| /** |
| * Calculate the internal maps used by the operation. |
| * |
| * <p>In addition to the return value, the following fields are populated as a side effect: |
| * |
| * <ul> |
| * <li>{@code affectedBranches} |
| * <li>{@code targets} |
| * <li>{@code branchesByProject} |
| * <li>{@code subscribedBranches} |
| * </ul> |
| * |
| * @return the ordered set to be stored in {@link #sortedBranches}. |
| */ |
| private Set<BranchNameKey> calculateSubscriptionMaps( |
| Set<BranchNameKey> updatedBranches, |
| Set<BranchNameKey> affectedBranches, |
| SetMultimap<BranchNameKey, SubmoduleSubscription> targets, |
| SetMultimap<Project.NameKey, BranchNameKey> branchesByProject, |
| Set<BranchNameKey> subscribedBranches, |
| Map<BranchNameKey, GitModules> branchGitModules, |
| MergeOpRepoManager orm) |
| throws SubmoduleConflictException { |
| LinkedHashSet<BranchNameKey> allVisited = new LinkedHashSet<>(); |
| for (BranchNameKey updatedBranch : updatedBranches) { |
| if (allVisited.contains(updatedBranch)) { |
| continue; |
| } |
| |
| searchForSuperprojects( |
| updatedBranch, |
| new LinkedHashSet<>(), |
| allVisited, |
| affectedBranches, |
| targets, |
| branchesByProject, |
| subscribedBranches, |
| branchGitModules, |
| orm); |
| } |
| |
| // Since the searchForSuperprojects will add all branches (related or |
| // unrelated) and ensure the superproject's branches get added first before |
| // a submodule branch. Need remove all unrelated branches and reverse |
| // the order. |
| allVisited.retainAll(affectedBranches); |
| reverse(allVisited); |
| return allVisited; |
| } |
| |
| private void searchForSuperprojects( |
| BranchNameKey current, |
| LinkedHashSet<BranchNameKey> currentVisited, |
| LinkedHashSet<BranchNameKey> allVisited, |
| Set<BranchNameKey> affectedBranches, |
| SetMultimap<BranchNameKey, SubmoduleSubscription> targets, |
| SetMultimap<Project.NameKey, BranchNameKey> branchesByProject, |
| Set<BranchNameKey> subscribedBranches, |
| Map<BranchNameKey, GitModules> branchGitModules, |
| MergeOpRepoManager orm) |
| throws SubmoduleConflictException { |
| logger.atFine().log("Now processing %s", current); |
| |
| if (currentVisited.contains(current)) { |
| throw new SubmoduleConflictException( |
| "Branch level circular subscriptions detected: " |
| + CircularPathFinder.printCircularPath(currentVisited, current)); |
| } |
| |
| if (allVisited.contains(current)) { |
| return; |
| } |
| |
| currentVisited.add(current); |
| try { |
| Collection<SubmoduleSubscription> subscriptions = |
| superProjectSubscriptionsForSubmoduleBranch(current, branchGitModules, orm); |
| for (SubmoduleSubscription sub : subscriptions) { |
| BranchNameKey superBranch = sub.getSuperProject(); |
| searchForSuperprojects( |
| superBranch, |
| currentVisited, |
| allVisited, |
| affectedBranches, |
| targets, |
| branchesByProject, |
| subscribedBranches, |
| branchGitModules, |
| orm); |
| targets.put(superBranch, sub); |
| branchesByProject.put(superBranch.project(), superBranch); |
| affectedBranches.add(superBranch); |
| affectedBranches.add(sub.getSubmodule()); |
| subscribedBranches.add(sub.getSubmodule()); |
| } |
| } catch (IOException e) { |
| throw new StorageException("Cannot find superprojects for " + current, e); |
| } |
| currentVisited.remove(current); |
| allVisited.add(current); |
| } |
| |
| private Collection<BranchNameKey> getDestinationBranches( |
| BranchNameKey src, SubscribeSection s, MergeOpRepoManager orm) throws IOException { |
| OpenRepo or; |
| try { |
| or = orm.getRepo(s.project()); |
| } catch (NoSuchProjectException e) { |
| // A project listed a non existent project to be allowed |
| // to subscribe to it. Allow this for now, i.e. no exception is |
| // thrown. |
| return s.getDestinationBranches(src, ImmutableList.of()); |
| } |
| |
| List<Ref> refs = or.repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS); |
| return s.getDestinationBranches(src, refs); |
| } |
| |
| private Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch( |
| BranchNameKey srcBranch, |
| Map<BranchNameKey, GitModules> branchGitModules, |
| MergeOpRepoManager orm) |
| throws IOException { |
| Collection<SubmoduleSubscription> ret = new ArrayList<>(); |
| if (RefNames.isGerritRef(srcBranch.branch())) return ret; |
| |
| Project.NameKey srcProject = srcBranch.project(); |
| for (SubscribeSection s : |
| projectCache |
| .get(srcProject) |
| .orElseThrow(illegalState(srcProject)) |
| .getSubscribeSections(srcBranch)) { |
| Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s, orm); |
| for (BranchNameKey targetBranch : branches) { |
| Project.NameKey targetProject = targetBranch.project(); |
| try { |
| OpenRepo or = orm.getRepo(targetProject); |
| ObjectId id = or.repo.resolve(targetBranch.branch()); |
| if (id == null) { |
| logger.atFine().log("SubscribeSection %s: branch %s doesn't exist.", s, targetBranch); |
| continue; |
| } |
| } catch (NoSuchProjectException e) { |
| logger.atFine().log("SubscribeSection %s: project %s doesn't exist", s, targetProject); |
| continue; |
| } |
| |
| GitModules m = branchGitModules.get(targetBranch); |
| if (m == null) { |
| m = gitmodulesFactory.create(targetBranch, orm); |
| branchGitModules.put(targetBranch, m); |
| } |
| ret.addAll(m.subscribedTo(srcBranch)); |
| } |
| } |
| logger.atFine().log("Calculated superprojects for %s are %s", srcBranch, ret); |
| return ret; |
| } |
| |
| private static <T> void reverse(LinkedHashSet<T> set) { |
| if (set == null) { |
| return; |
| } |
| |
| Deque<T> q = new ArrayDeque<>(set); |
| set.clear(); |
| |
| while (!q.isEmpty()) { |
| set.add(q.removeLast()); |
| } |
| } |
| } |
| } |