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 : }