LCOV - code coverage report
Current view: top level - server/submit - MergeSuperSet.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 70 77 90.9 %
Date: 2022-11-19 15:00:39 Functions: 8 8 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2015 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.common.base.Preconditions.checkState;
      18             : import static java.util.Objects.requireNonNull;
      19             : 
      20             : import com.google.common.base.Strings;
      21             : import com.google.gerrit.entities.Change;
      22             : import com.google.gerrit.extensions.registration.DynamicItem;
      23             : import com.google.gerrit.extensions.restapi.AuthException;
      24             : import com.google.gerrit.server.CurrentUser;
      25             : import com.google.gerrit.server.config.GerritServerConfig;
      26             : import com.google.gerrit.server.logging.TraceContext;
      27             : import com.google.gerrit.server.permissions.ChangePermission;
      28             : import com.google.gerrit.server.permissions.PermissionBackend;
      29             : import com.google.gerrit.server.permissions.PermissionBackendException;
      30             : import com.google.gerrit.server.plugincontext.PluginContext;
      31             : import com.google.gerrit.server.project.ProjectCache;
      32             : import com.google.gerrit.server.project.ProjectState;
      33             : import com.google.gerrit.server.query.change.ChangeData;
      34             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      35             : import com.google.inject.Inject;
      36             : import com.google.inject.Provider;
      37             : import java.io.IOException;
      38             : import java.util.ArrayList;
      39             : import java.util.HashSet;
      40             : import java.util.List;
      41             : import java.util.Set;
      42             : import org.eclipse.jgit.lib.Config;
      43             : 
      44             : /**
      45             :  * Calculates the minimal superset of changes required to be merged.
      46             :  *
      47             :  * <p>This includes all parents between a change and the tip of its target branch for the
      48             :  * merging/rebasing submit strategies. For the cherry-pick strategy no additional changes are
      49             :  * included.
      50             :  *
      51             :  * <p>If change.submitWholeTopic is enabled, also all changes of the topic and their parents are
      52             :  * included.
      53             :  */
      54             : public class MergeSuperSet {
      55             :   private final ChangeData.Factory changeDataFactory;
      56             :   private final Provider<InternalChangeQuery> queryProvider;
      57             :   private final Provider<MergeOpRepoManager> repoManagerProvider;
      58             :   private final DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation;
      59             :   private final PermissionBackend permissionBackend;
      60             :   private final Config cfg;
      61             :   private final ProjectCache projectCache;
      62             : 
      63             :   private MergeOpRepoManager orm;
      64             :   private boolean closeOrm;
      65             : 
      66             :   @Inject
      67             :   MergeSuperSet(
      68             :       @GerritServerConfig Config cfg,
      69             :       ChangeData.Factory changeDataFactory,
      70             :       Provider<InternalChangeQuery> queryProvider,
      71             :       Provider<MergeOpRepoManager> repoManagerProvider,
      72             :       DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation,
      73             :       PermissionBackend permissionBackend,
      74          53 :       ProjectCache projectCache) {
      75          53 :     this.cfg = cfg;
      76          53 :     this.changeDataFactory = changeDataFactory;
      77          53 :     this.queryProvider = queryProvider;
      78          53 :     this.repoManagerProvider = repoManagerProvider;
      79          53 :     this.mergeSuperSetComputation = mergeSuperSetComputation;
      80          53 :     this.permissionBackend = permissionBackend;
      81          53 :     this.projectCache = projectCache;
      82          53 :   }
      83             : 
      84             :   public static boolean wholeTopicEnabled(Config config) {
      85         145 :     return config.getBoolean("change", null, "submitWholeTopic", false);
      86             :   }
      87             : 
      88             :   public MergeSuperSet setMergeOpRepoManager(MergeOpRepoManager orm) {
      89          53 :     checkState(this.orm == null);
      90          53 :     this.orm = requireNonNull(orm);
      91          53 :     closeOrm = false;
      92          53 :     return this;
      93             :   }
      94             : 
      95             :   /**
      96             :    * Gets the ChangeSet of this {@code change} based on visiblity of the {@code user}. if
      97             :    * change.submitWholeTopic is true, we return the topic closure as well as the dependent changes
      98             :    * of the topic closure. Otherwise, we return just the dependent changes.
      99             :    *
     100             :    * @param change the change for which we get the dependent changes / topic closure.
     101             :    * @param user the current user for visibility purposes.
     102             :    * @param includingTopicClosure when true, return as if change.submitWholeTopic = true, so we
     103             :    *     return the topic closure.
     104             :    * @return {@link ChangeSet} object that represents the dependent changes and/or topic closure of
     105             :    *     the requested change.
     106             :    */
     107             :   public ChangeSet completeChangeSet(Change change, CurrentUser user, boolean includingTopicClosure)
     108             :       throws IOException, PermissionBackendException {
     109             :     try {
     110          53 :       if (orm == null) {
     111          21 :         orm = repoManagerProvider.get();
     112          21 :         closeOrm = true;
     113             :       }
     114          53 :       ChangeData cd = changeDataFactory.create(change.getProject(), change.getId());
     115          53 :       boolean visible = false;
     116          53 :       if (cd != null) {
     117          53 :         if (projectCache.get(cd.project()).map(ProjectState::statePermitsRead).orElse(false)) {
     118             :           try {
     119          53 :             permissionBackend.user(user).change(cd).check(ChangePermission.READ);
     120          53 :             visible = true;
     121           0 :           } catch (AuthException e) {
     122             :             // Do nothing.
     123          53 :           }
     124             :         }
     125             :       }
     126             : 
     127          53 :       ChangeSet changeSet = new ChangeSet(cd, visible);
     128          53 :       if (wholeTopicEnabled(cfg) || includingTopicClosure) {
     129          15 :         return completeChangeSetIncludingTopics(changeSet, user);
     130             :       }
     131          50 :       try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation)) {
     132          50 :         return mergeSuperSetComputation.get().completeWithoutTopic(orm, changeSet, user);
     133             :       }
     134             :     } finally {
     135          53 :       if (closeOrm && orm != null) {
     136          21 :         orm.close();
     137          21 :         orm = null;
     138             :       }
     139             :     }
     140             :   }
     141             : 
     142             :   /**
     143             :    * Completes {@code changeSet} with any additional changes from its topics
     144             :    *
     145             :    * <p>{@link #completeChangeSetIncludingTopics} calls this repeatedly, alternating with {@link
     146             :    * MergeSuperSetComputation#completeWithoutTopic(MergeOpRepoManager, ChangeSet, CurrentUser)}, to
     147             :    * discover what additional changes should be submitted with a change until the set stops growing.
     148             :    *
     149             :    * <p>{@code topicsSeen} and {@code visibleTopicsSeen} keep track of topics already explored to
     150             :    * avoid wasted work.
     151             :    *
     152             :    * @return the resulting larger {@link ChangeSet}
     153             :    */
     154             :   private ChangeSet topicClosure(
     155             :       ChangeSet changeSet, CurrentUser user, Set<String> topicsSeen, Set<String> visibleTopicsSeen)
     156             :       throws PermissionBackendException {
     157          15 :     List<ChangeData> visibleChanges = new ArrayList<>();
     158          15 :     List<ChangeData> nonVisibleChanges = new ArrayList<>();
     159             : 
     160          15 :     for (ChangeData cd : changeSet.changes()) {
     161          15 :       visibleChanges.add(cd);
     162          15 :       String topic = cd.change().getTopic();
     163          15 :       if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) {
     164          15 :         continue;
     165             :       }
     166          15 :       for (ChangeData topicCd : byTopicOpen(topic)) {
     167          15 :         if (canRead(user, topicCd)) {
     168          15 :           visibleChanges.add(topicCd);
     169             :         } else {
     170           7 :           nonVisibleChanges.add(topicCd);
     171             :         }
     172          15 :       }
     173          15 :       topicsSeen.add(topic);
     174          15 :       visibleTopicsSeen.add(topic);
     175          15 :     }
     176          15 :     for (ChangeData cd : changeSet.nonVisibleChanges()) {
     177           7 :       nonVisibleChanges.add(cd);
     178           7 :       String topic = cd.change().getTopic();
     179           7 :       if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) {
     180           7 :         continue;
     181             :       }
     182           0 :       for (ChangeData topicCd : byTopicOpen(topic)) {
     183           0 :         nonVisibleChanges.add(topicCd);
     184           0 :       }
     185           0 :       topicsSeen.add(topic);
     186           0 :     }
     187          15 :     return new ChangeSet(visibleChanges, nonVisibleChanges);
     188             :   }
     189             : 
     190             :   private ChangeSet completeChangeSetIncludingTopics(ChangeSet changeSet, CurrentUser user)
     191             :       throws IOException, PermissionBackendException {
     192          15 :     Set<String> topicsSeen = new HashSet<>();
     193          15 :     Set<String> visibleTopicsSeen = new HashSet<>();
     194             :     int oldSeen;
     195             :     int seen;
     196             : 
     197          15 :     changeSet = topicClosure(changeSet, user, topicsSeen, visibleTopicsSeen);
     198          15 :     seen = topicsSeen.size() + visibleTopicsSeen.size();
     199             : 
     200             :     do {
     201          15 :       oldSeen = seen;
     202          15 :       try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation)) {
     203          15 :         changeSet = mergeSuperSetComputation.get().completeWithoutTopic(orm, changeSet, user);
     204             :       }
     205          15 :       changeSet = topicClosure(changeSet, user, topicsSeen, visibleTopicsSeen);
     206          15 :       seen = topicsSeen.size() + visibleTopicsSeen.size();
     207          15 :     } while (seen != oldSeen);
     208          15 :     return changeSet;
     209             :   }
     210             : 
     211             :   private List<ChangeData> byTopicOpen(String topic) {
     212          15 :     return queryProvider.get().byTopicOpen(topic);
     213             :   }
     214             : 
     215             :   private boolean canRead(CurrentUser user, ChangeData cd) throws PermissionBackendException {
     216          15 :     if (!projectCache.get(cd.project()).map(ProjectState::statePermitsRead).orElse(false)) {
     217           0 :       return false;
     218             :     }
     219             :     try {
     220          15 :       permissionBackend.user(user).change(cd).check(ChangePermission.READ);
     221          15 :       return true;
     222           7 :     } catch (AuthException e) {
     223           7 :       return false;
     224             :     }
     225             :   }
     226             : }

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