LCOV - code coverage report
Current view: top level - server/submit - SubscriptionGraph.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 106 109 97.2 %
Date: 2022-11-19 15:00:39 Functions: 20 20 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2020 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.submit;
      16             : 
      17             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      18             : 
      19             : import com.google.common.collect.ImmutableList;
      20             : import com.google.common.collect.ImmutableSet;
      21             : import com.google.common.collect.ImmutableSetMultimap;
      22             : import com.google.common.collect.MultimapBuilder;
      23             : import com.google.common.collect.SetMultimap;
      24             : import com.google.common.flogger.FluentLogger;
      25             : import com.google.gerrit.common.UsedAt;
      26             : import com.google.gerrit.entities.BranchNameKey;
      27             : import com.google.gerrit.entities.Project;
      28             : import com.google.gerrit.entities.RefNames;
      29             : import com.google.gerrit.entities.SubmoduleSubscription;
      30             : import com.google.gerrit.entities.SubscribeSection;
      31             : import com.google.gerrit.exceptions.StorageException;
      32             : import com.google.gerrit.server.project.NoSuchProjectException;
      33             : import com.google.gerrit.server.project.ProjectCache;
      34             : import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
      35             : import com.google.inject.AbstractModule;
      36             : import com.google.inject.Inject;
      37             : import java.io.IOException;
      38             : import java.util.ArrayDeque;
      39             : import java.util.ArrayList;
      40             : import java.util.Collection;
      41             : import java.util.Deque;
      42             : import java.util.HashMap;
      43             : import java.util.HashSet;
      44             : import java.util.LinkedHashSet;
      45             : import java.util.List;
      46             : import java.util.Map;
      47             : import java.util.Set;
      48             : import org.eclipse.jgit.lib.ObjectId;
      49             : import org.eclipse.jgit.lib.Ref;
      50             : 
      51             : /**
      52             :  * A container which stores subscription relationship. A SubscriptionGraph is calculated every time
      53             :  * changes are pushed. Some branches are updated in these changes, and if these branches are
      54             :  * subscribed by other projects, SubscriptionGraph would record information about these updated
      55             :  * branches and branches/projects affected.
      56             :  */
      57             : public class SubscriptionGraph {
      58             :   /** Branches updated as part of the enclosing submit or push batch. */
      59             :   private final ImmutableSet<BranchNameKey> updatedBranches;
      60             : 
      61             :   /**
      62             :    * All branches affected, including those in superprojects and submodules, sorted by submodule
      63             :    * traversal order. To support nested subscriptions, GitLink commits need to be updated in order.
      64             :    * The closer to topological "leaf", the earlier a commit should be updated.
      65             :    *
      66             :    * <p>For example, there are three projects, top level project p1 subscribed to p2, p2 subscribed
      67             :    * to bottom level project p3. When submit a change for p3. We need update both p2 and p1. To be
      68             :    * more precise, we need update p2 first and then update p1.
      69             :    */
      70             :   private final ImmutableSet<BranchNameKey> sortedBranches;
      71             : 
      72             :   /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
      73             :   private final ImmutableSetMultimap<BranchNameKey, SubmoduleSubscription> targets;
      74             : 
      75             :   /**
      76             :    * Multimap of superproject name to all branch names within that superproject which have submodule
      77             :    * subscriptions.
      78             :    */
      79             :   private final ImmutableSetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
      80             : 
      81             :   /** All branches subscribed by other projects. */
      82             :   private final ImmutableSet<BranchNameKey> subscribedBranches;
      83             : 
      84             :   public SubscriptionGraph(
      85             :       Set<BranchNameKey> updatedBranches,
      86             :       SetMultimap<BranchNameKey, SubmoduleSubscription> targets,
      87             :       SetMultimap<Project.NameKey, BranchNameKey> branchesByProject,
      88             :       Set<BranchNameKey> subscribedBranches,
      89          69 :       Set<BranchNameKey> sortedBranches) {
      90          69 :     this.updatedBranches = ImmutableSet.copyOf(updatedBranches);
      91          69 :     this.targets = ImmutableSetMultimap.copyOf(targets);
      92          69 :     this.branchesByProject = ImmutableSetMultimap.copyOf(branchesByProject);
      93          69 :     this.subscribedBranches = ImmutableSet.copyOf(subscribedBranches);
      94          69 :     this.sortedBranches = ImmutableSet.copyOf(sortedBranches);
      95          69 :   }
      96             : 
      97             :   /** Returns an empty {@code SubscriptionGraph}. */
      98             :   static SubscriptionGraph createEmptyGraph(Set<BranchNameKey> updatedBranches) {
      99           1 :     return new SubscriptionGraph(
     100             :         updatedBranches,
     101           1 :         ImmutableSetMultimap.of(),
     102           1 :         ImmutableSetMultimap.of(),
     103           1 :         ImmutableSet.of(),
     104           1 :         ImmutableSet.of());
     105             :   }
     106             : 
     107             :   /** Get branches updated as part of the enclosing submit or push batch. */
     108             :   public ImmutableSet<BranchNameKey> getUpdatedBranches() {
     109          68 :     return updatedBranches;
     110             :   }
     111             : 
     112             :   /** Get all superprojects affected. */
     113             :   public ImmutableSet<Project.NameKey> getAffectedSuperProjects() {
     114          69 :     return branchesByProject.keySet();
     115             :   }
     116             : 
     117             :   /** See if a {@code project} is a superproject affected. */
     118             :   boolean isAffectedSuperProject(Project.NameKey project) {
     119          68 :     return branchesByProject.containsKey(project);
     120             :   }
     121             : 
     122             :   /**
     123             :    * Returns all branches within the superproject {@code project} which have submodule
     124             :    * subscriptions.
     125             :    */
     126             :   public ImmutableSet<BranchNameKey> getAffectedSuperBranches(Project.NameKey project) {
     127           3 :     return branchesByProject.get(project);
     128             :   }
     129             : 
     130             :   /**
     131             :    * Get all affected branches, including the submodule branches and superproject branches, sorted
     132             :    * by traversal order.
     133             :    *
     134             :    * @see SubscriptionGraph#sortedBranches
     135             :    */
     136             :   public ImmutableSet<BranchNameKey> getSortedSuperprojectAndSubmoduleBranches() {
     137          54 :     return sortedBranches;
     138             :   }
     139             : 
     140             :   /** Check if a {@code branch} is a submodule of a superproject. */
     141             :   @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
     142             :   public boolean hasSuperproject(BranchNameKey branch) {
     143           1 :     return subscribedBranches.contains(branch);
     144             :   }
     145             : 
     146             :   /** See if a {@code branch} is a superproject branch affected. */
     147             :   public boolean hasSubscription(BranchNameKey branch) {
     148          53 :     return targets.containsKey(branch);
     149             :   }
     150             : 
     151             :   /** Get all related {@code SubmoduleSubscription}s whose super branch is {@code branch}. */
     152             :   public ImmutableSet<SubmoduleSubscription> getSubscriptions(BranchNameKey branch) {
     153           3 :     return targets.get(branch);
     154             :   }
     155             : 
     156             :   public interface Factory {
     157             :     SubscriptionGraph compute(Set<BranchNameKey> updatedBranches, MergeOpRepoManager orm)
     158             :         throws SubmoduleConflictException;
     159             :   }
     160             : 
     161         152 :   public static class SubscriptionGraphModule extends AbstractModule {
     162             :     @Override
     163             :     protected void configure() {
     164         152 :       bind(Factory.class).annotatedWith(VanillaSubscriptionGraph.class).to(DefaultFactory.class);
     165         152 :     }
     166             :   }
     167             : 
     168             :   public static class DefaultFactory implements Factory {
     169         143 :     private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     170             :     private final ProjectCache projectCache;
     171             :     private final GitModules.Factory gitmodulesFactory;
     172             : 
     173             :     @Inject
     174         143 :     DefaultFactory(GitModules.Factory gitmodulesFactory, ProjectCache projectCache) {
     175         143 :       this.gitmodulesFactory = gitmodulesFactory;
     176         143 :       this.projectCache = projectCache;
     177         143 :     }
     178             : 
     179             :     @Override
     180             :     public SubscriptionGraph compute(Set<BranchNameKey> updatedBranches, MergeOpRepoManager orm)
     181             :         throws SubmoduleConflictException {
     182          69 :       Map<BranchNameKey, GitModules> branchGitModules = new HashMap<>();
     183             :       // All affected branches, including those in superprojects and submodules.
     184          69 :       Set<BranchNameKey> affectedBranches = new HashSet<>();
     185             : 
     186             :       // See SubscriptionGraph#targets.
     187             :       SetMultimap<BranchNameKey, SubmoduleSubscription> targets =
     188          69 :           MultimapBuilder.hashKeys().hashSetValues().build();
     189             : 
     190             :       // See SubscriptionGraph#branchesByProject.
     191             :       SetMultimap<Project.NameKey, BranchNameKey> branchesByProject =
     192          69 :           MultimapBuilder.hashKeys().hashSetValues().build();
     193             : 
     194             :       // See SubscriptionGraph#subscribedBranches.
     195          69 :       Set<BranchNameKey> subscribedBranches = new HashSet<>();
     196             : 
     197          69 :       Set<BranchNameKey> sortedBranches =
     198          69 :           calculateSubscriptionMaps(
     199             :               updatedBranches,
     200             :               affectedBranches,
     201             :               targets,
     202             :               branchesByProject,
     203             :               subscribedBranches,
     204             :               branchGitModules,
     205             :               orm);
     206             : 
     207          69 :       return new SubscriptionGraph(
     208             :           updatedBranches, targets, branchesByProject, subscribedBranches, sortedBranches);
     209             :     }
     210             : 
     211             :     /**
     212             :      * Calculate the internal maps used by the operation.
     213             :      *
     214             :      * <p>In addition to the return value, the following fields are populated as a side effect:
     215             :      *
     216             :      * <ul>
     217             :      *   <li>{@code affectedBranches}
     218             :      *   <li>{@code targets}
     219             :      *   <li>{@code branchesByProject}
     220             :      *   <li>{@code subscribedBranches}
     221             :      * </ul>
     222             :      *
     223             :      * @return the ordered set to be stored in {@link #sortedBranches}.
     224             :      */
     225             :     private Set<BranchNameKey> calculateSubscriptionMaps(
     226             :         Set<BranchNameKey> updatedBranches,
     227             :         Set<BranchNameKey> affectedBranches,
     228             :         SetMultimap<BranchNameKey, SubmoduleSubscription> targets,
     229             :         SetMultimap<Project.NameKey, BranchNameKey> branchesByProject,
     230             :         Set<BranchNameKey> subscribedBranches,
     231             :         Map<BranchNameKey, GitModules> branchGitModules,
     232             :         MergeOpRepoManager orm)
     233             :         throws SubmoduleConflictException {
     234          69 :       LinkedHashSet<BranchNameKey> allVisited = new LinkedHashSet<>();
     235          69 :       for (BranchNameKey updatedBranch : updatedBranches) {
     236          69 :         if (allVisited.contains(updatedBranch)) {
     237           1 :           continue;
     238             :         }
     239             : 
     240          69 :         searchForSuperprojects(
     241             :             updatedBranch,
     242             :             new LinkedHashSet<>(),
     243             :             allVisited,
     244             :             affectedBranches,
     245             :             targets,
     246             :             branchesByProject,
     247             :             subscribedBranches,
     248             :             branchGitModules,
     249             :             orm);
     250          69 :       }
     251             : 
     252             :       // Since the searchForSuperprojects will add all branches (related or
     253             :       // unrelated) and ensure the superproject's branches get added first before
     254             :       // a submodule branch. Need remove all unrelated branches and reverse
     255             :       // the order.
     256          69 :       allVisited.retainAll(affectedBranches);
     257          69 :       reverse(allVisited);
     258          69 :       return allVisited;
     259             :     }
     260             : 
     261             :     private void searchForSuperprojects(
     262             :         BranchNameKey current,
     263             :         LinkedHashSet<BranchNameKey> currentVisited,
     264             :         LinkedHashSet<BranchNameKey> allVisited,
     265             :         Set<BranchNameKey> affectedBranches,
     266             :         SetMultimap<BranchNameKey, SubmoduleSubscription> targets,
     267             :         SetMultimap<Project.NameKey, BranchNameKey> branchesByProject,
     268             :         Set<BranchNameKey> subscribedBranches,
     269             :         Map<BranchNameKey, GitModules> branchGitModules,
     270             :         MergeOpRepoManager orm)
     271             :         throws SubmoduleConflictException {
     272          69 :       logger.atFine().log("Now processing %s", current);
     273             : 
     274          69 :       if (currentVisited.contains(current)) {
     275           3 :         throw new SubmoduleConflictException(
     276             :             "Branch level circular subscriptions detected:  "
     277           3 :                 + CircularPathFinder.printCircularPath(currentVisited, current));
     278             :       }
     279             : 
     280          69 :       if (allVisited.contains(current)) {
     281           3 :         return;
     282             :       }
     283             : 
     284          69 :       currentVisited.add(current);
     285             :       try {
     286          69 :         Collection<SubmoduleSubscription> subscriptions =
     287          69 :             superProjectSubscriptionsForSubmoduleBranch(current, branchGitModules, orm);
     288          69 :         for (SubmoduleSubscription sub : subscriptions) {
     289           3 :           BranchNameKey superBranch = sub.getSuperProject();
     290           3 :           searchForSuperprojects(
     291             :               superBranch,
     292             :               currentVisited,
     293             :               allVisited,
     294             :               affectedBranches,
     295             :               targets,
     296             :               branchesByProject,
     297             :               subscribedBranches,
     298             :               branchGitModules,
     299             :               orm);
     300           3 :           targets.put(superBranch, sub);
     301           3 :           branchesByProject.put(superBranch.project(), superBranch);
     302           3 :           affectedBranches.add(superBranch);
     303           3 :           affectedBranches.add(sub.getSubmodule());
     304           3 :           subscribedBranches.add(sub.getSubmodule());
     305           3 :         }
     306           0 :       } catch (IOException e) {
     307           0 :         throw new StorageException("Cannot find superprojects for " + current, e);
     308          69 :       }
     309          69 :       currentVisited.remove(current);
     310          69 :       allVisited.add(current);
     311          69 :     }
     312             : 
     313             :     private Collection<BranchNameKey> getDestinationBranches(
     314             :         BranchNameKey src, SubscribeSection s, MergeOpRepoManager orm) throws IOException {
     315             :       OpenRepo or;
     316             :       try {
     317           3 :         or = orm.getRepo(s.project());
     318           1 :       } catch (NoSuchProjectException e) {
     319             :         // A project listed a non existent project to be allowed
     320             :         // to subscribe to it. Allow this for now, i.e. no exception is
     321             :         // thrown.
     322           1 :         return s.getDestinationBranches(src, ImmutableList.of());
     323           3 :       }
     324             : 
     325           3 :       List<Ref> refs = or.repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS);
     326           3 :       return s.getDestinationBranches(src, refs);
     327             :     }
     328             : 
     329             :     private Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
     330             :         BranchNameKey srcBranch,
     331             :         Map<BranchNameKey, GitModules> branchGitModules,
     332             :         MergeOpRepoManager orm)
     333             :         throws IOException {
     334          69 :       Collection<SubmoduleSubscription> ret = new ArrayList<>();
     335          69 :       if (RefNames.isGerritRef(srcBranch.branch())) return ret;
     336             : 
     337          66 :       Project.NameKey srcProject = srcBranch.project();
     338             :       for (SubscribeSection s :
     339             :           projectCache
     340          66 :               .get(srcProject)
     341          66 :               .orElseThrow(illegalState(srcProject))
     342          66 :               .getSubscribeSections(srcBranch)) {
     343           3 :         Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s, orm);
     344           3 :         for (BranchNameKey targetBranch : branches) {
     345           3 :           Project.NameKey targetProject = targetBranch.project();
     346             :           try {
     347           3 :             OpenRepo or = orm.getRepo(targetProject);
     348           3 :             ObjectId id = or.repo.resolve(targetBranch.branch());
     349           3 :             if (id == null) {
     350           1 :               logger.atFine().log("SubscribeSection %s: branch %s doesn't exist.", s, targetBranch);
     351           1 :               continue;
     352             :             }
     353           1 :           } catch (NoSuchProjectException e) {
     354           1 :             logger.atFine().log("SubscribeSection %s: project %s doesn't exist", s, targetProject);
     355           1 :             continue;
     356           3 :           }
     357             : 
     358           3 :           GitModules m = branchGitModules.get(targetBranch);
     359           3 :           if (m == null) {
     360           3 :             m = gitmodulesFactory.create(targetBranch, orm);
     361           3 :             branchGitModules.put(targetBranch, m);
     362             :           }
     363           3 :           ret.addAll(m.subscribedTo(srcBranch));
     364           3 :         }
     365           3 :       }
     366          66 :       logger.atFine().log("Calculated superprojects for %s are %s", srcBranch, ret);
     367          66 :       return ret;
     368             :     }
     369             : 
     370             :     private static <T> void reverse(LinkedHashSet<T> set) {
     371          69 :       if (set == null) {
     372           0 :         return;
     373             :       }
     374             : 
     375          69 :       Deque<T> q = new ArrayDeque<>(set);
     376          69 :       set.clear();
     377             : 
     378          69 :       while (!q.isEmpty()) {
     379           3 :         set.add(q.removeLast());
     380             :       }
     381          69 :     }
     382             :   }
     383             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750