Line data Source code
1 : // Copyright (C) 2017 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 :
19 : import com.google.auto.value.AutoValue;
20 : import com.google.common.collect.ImmutableList;
21 : import com.google.common.collect.ImmutableListMultimap;
22 : import com.google.common.collect.ImmutableSet;
23 : import com.google.common.collect.Iterables;
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.SubmitTypeRecord;
29 : import com.google.gerrit.exceptions.StorageException;
30 : import com.google.gerrit.extensions.client.SubmitType;
31 : import com.google.gerrit.extensions.registration.DynamicItem;
32 : import com.google.gerrit.server.CurrentUser;
33 : import com.google.gerrit.server.config.GerritServerConfig;
34 : import com.google.gerrit.server.project.NoSuchProjectException;
35 : import com.google.gerrit.server.query.change.ChangeData;
36 : import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
37 : import com.google.gerrit.server.query.change.InternalChangeQuery;
38 : import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
39 : import com.google.inject.AbstractModule;
40 : import com.google.inject.Inject;
41 : import com.google.inject.Provider;
42 : import java.io.IOException;
43 : import java.util.ArrayList;
44 : import java.util.Collection;
45 : import java.util.Collections;
46 : import java.util.HashMap;
47 : import java.util.HashSet;
48 : import java.util.List;
49 : import java.util.Map;
50 : import java.util.Optional;
51 : import java.util.Set;
52 : import org.eclipse.jgit.lib.Config;
53 : import org.eclipse.jgit.lib.Ref;
54 : import org.eclipse.jgit.revwalk.RevCommit;
55 : import org.eclipse.jgit.revwalk.RevSort;
56 :
57 : /**
58 : * Default implementation of MergeSuperSet that does the computation of the merge super set
59 : * sequentially on the local Gerrit instance.
60 : */
61 : public class LocalMergeSuperSetComputation implements MergeSuperSetComputation {
62 53 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
63 :
64 : public static final int MAX_SUBMITTABLE_CHANGES_AT_ONCE_DEFAULT = 1024;
65 :
66 152 : public static class LocalMergeSuperSetComputationModule extends AbstractModule {
67 : @Override
68 : protected void configure() {
69 152 : DynamicItem.bind(binder(), MergeSuperSetComputation.class)
70 152 : .to(LocalMergeSuperSetComputation.class);
71 152 : }
72 : }
73 :
74 : @AutoValue
75 53 : abstract static class QueryKey {
76 : private static QueryKey create(BranchNameKey branch, Iterable<String> hashes) {
77 53 : return new AutoValue_LocalMergeSuperSetComputation_QueryKey(
78 53 : branch, ImmutableSet.copyOf(hashes));
79 : }
80 :
81 : abstract BranchNameKey branch();
82 :
83 : abstract ImmutableSet<String> hashes();
84 : }
85 :
86 : private final Provider<InternalChangeQuery> queryProvider;
87 : private final Map<QueryKey, ImmutableList<ChangeData>> queryCache;
88 : private final Map<BranchNameKey, Optional<RevCommit>> heads;
89 : private final ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory;
90 : private final int maxSubmittableChangesAtOnce;
91 :
92 : @Inject
93 : LocalMergeSuperSetComputation(
94 : Provider<InternalChangeQuery> queryProvider,
95 : ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory,
96 53 : @GerritServerConfig Config gerritConfig) {
97 53 : this.queryProvider = queryProvider;
98 53 : this.queryCache = new HashMap<>();
99 53 : this.heads = new HashMap<>();
100 53 : this.changeIsVisibleToPredicateFactory = changeIsVisibleToPredicateFactory;
101 53 : this.maxSubmittableChangesAtOnce =
102 53 : gerritConfig.getInt(
103 : "change", "maxSubmittableAtOnce", MAX_SUBMITTABLE_CHANGES_AT_ONCE_DEFAULT);
104 53 : }
105 :
106 : @Override
107 : public ChangeSet completeWithoutTopic(
108 : MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user) throws IOException {
109 53 : Collection<ChangeData> visibleChanges = new ArrayList<>();
110 53 : Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
111 :
112 : // For each target branch we run a separate rev walk to find open changes
113 : // reachable from changes already in the merge super set.
114 53 : ImmutableSet<BranchNameKey> branches =
115 53 : byBranch(Iterables.concat(changeSet.changes(), changeSet.nonVisibleChanges())).keySet();
116 53 : ImmutableListMultimap<BranchNameKey, ChangeData> visibleChangesPerBranch =
117 53 : byBranch(changeSet.changes());
118 53 : ImmutableListMultimap<BranchNameKey, ChangeData> nonVisibleChangesPerBranch =
119 53 : byBranch(changeSet.nonVisibleChanges());
120 :
121 53 : for (BranchNameKey branchNameKey : branches) {
122 53 : OpenRepo or = getRepo(orm, branchNameKey.project());
123 53 : List<RevCommit> visibleCommits = new ArrayList<>();
124 53 : List<RevCommit> nonVisibleCommits = new ArrayList<>();
125 :
126 53 : for (ChangeData cd : visibleChangesPerBranch.get(branchNameKey)) {
127 53 : if (submitType(cd) == SubmitType.CHERRY_PICK) {
128 8 : visibleChanges.add(cd);
129 : } else {
130 53 : visibleCommits.add(or.rw.parseCommit(cd.currentPatchSet().commitId()));
131 : }
132 53 : }
133 53 : for (ChangeData cd : nonVisibleChangesPerBranch.get(branchNameKey)) {
134 7 : if (submitType(cd) == SubmitType.CHERRY_PICK) {
135 1 : nonVisibleChanges.add(cd);
136 : } else {
137 6 : nonVisibleCommits.add(or.rw.parseCommit(cd.currentPatchSet().commitId()));
138 : }
139 7 : }
140 :
141 53 : Set<String> visibleHashes =
142 53 : walkChangesByHashes(
143 : visibleCommits,
144 53 : Collections.emptySet(),
145 : or,
146 : branchNameKey,
147 : maxSubmittableChangesAtOnce);
148 53 : Set<String> nonVisibleHashes =
149 53 : walkChangesByHashes(
150 : nonVisibleCommits, visibleHashes, or, branchNameKey, maxSubmittableChangesAtOnce);
151 :
152 53 : ChangeSet partialSet =
153 53 : byCommitsOnBranchNotMerged(or, branchNameKey, visibleHashes, nonVisibleHashes, user);
154 53 : Iterables.addAll(visibleChanges, partialSet.changes());
155 53 : Iterables.addAll(nonVisibleChanges, partialSet.nonVisibleChanges());
156 53 : }
157 :
158 53 : return new ChangeSet(visibleChanges, nonVisibleChanges);
159 : }
160 :
161 : private static ImmutableListMultimap<BranchNameKey, ChangeData> byBranch(
162 : Iterable<ChangeData> changes) {
163 : ImmutableListMultimap.Builder<BranchNameKey, ChangeData> builder =
164 53 : ImmutableListMultimap.builder();
165 53 : for (ChangeData cd : changes) {
166 53 : builder.put(cd.change().getDest(), cd);
167 53 : }
168 53 : return builder.build();
169 : }
170 :
171 : private OpenRepo getRepo(MergeOpRepoManager orm, Project.NameKey project) throws IOException {
172 : try {
173 53 : OpenRepo or = orm.getRepo(project);
174 53 : checkState(or.rw.hasRevSort(RevSort.TOPO));
175 53 : return or;
176 0 : } catch (NoSuchProjectException e) {
177 0 : throw new IOException(e);
178 : }
179 : }
180 :
181 : private SubmitType submitType(ChangeData cd) {
182 53 : SubmitTypeRecord str = cd.submitTypeRecord();
183 53 : if (!str.isOk()) {
184 0 : logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
185 : }
186 53 : return str.type;
187 : }
188 :
189 : @UsedAt(UsedAt.Project.GOOGLE)
190 : public ChangeSet byCommitsOnBranchNotMerged(
191 : OpenRepo or,
192 : BranchNameKey branch,
193 : Set<String> visibleHashes,
194 : Set<String> nonVisibleHashes,
195 : CurrentUser user)
196 : throws IOException {
197 53 : List<ChangeData> potentiallyVisibleChanges =
198 53 : byCommitsOnBranchNotMerged(or, branch, visibleHashes);
199 53 : List<ChangeData> invisibleChanges =
200 53 : new ArrayList<>(byCommitsOnBranchNotMerged(or, branch, nonVisibleHashes));
201 53 : List<ChangeData> visibleChanges = new ArrayList<>(potentiallyVisibleChanges.size());
202 53 : ChangeIsVisibleToPredicate changeIsVisibleToPredicate =
203 53 : changeIsVisibleToPredicateFactory.forUser(user);
204 53 : for (ChangeData cd : potentiallyVisibleChanges) {
205 : // short circuit permission checks for non-private changes, as we already checked all
206 : // permissions (except for private changes).
207 53 : if (!cd.change().isPrivate() || changeIsVisibleToPredicate.match(cd)) {
208 53 : visibleChanges.add(cd);
209 : } else {
210 2 : invisibleChanges.add(cd);
211 : }
212 53 : }
213 53 : return new ChangeSet(visibleChanges, invisibleChanges);
214 : }
215 :
216 : private ImmutableList<ChangeData> byCommitsOnBranchNotMerged(
217 : OpenRepo or, BranchNameKey branch, Set<String> hashes) throws IOException {
218 53 : if (hashes.isEmpty()) {
219 53 : return ImmutableList.of();
220 : }
221 53 : QueryKey k = QueryKey.create(branch, hashes);
222 53 : if (queryCache.containsKey(k)) {
223 0 : return queryCache.get(k);
224 : }
225 53 : ImmutableList<ChangeData> result =
226 53 : ImmutableList.copyOf(
227 53 : queryProvider.get().byCommitsOnBranchNotMerged(or.repo, branch, hashes));
228 53 : queryCache.put(k, result);
229 53 : return result;
230 : }
231 :
232 : @UsedAt(UsedAt.Project.GOOGLE)
233 : public Set<String> walkChangesByHashes(
234 : Collection<RevCommit> sourceCommits,
235 : Set<String> ignoreHashes,
236 : OpenRepo or,
237 : BranchNameKey b,
238 : int limit)
239 : throws IOException {
240 53 : Set<String> destHashes = new HashSet<>();
241 53 : or.rw.reset();
242 53 : markHeadUninteresting(or, b);
243 53 : for (RevCommit c : sourceCommits) {
244 53 : String name = c.name();
245 53 : if (ignoreHashes.contains(name)) {
246 0 : continue;
247 : }
248 53 : if (destHashes.size() < limit) {
249 53 : destHashes.add(name);
250 : } else {
251 : break;
252 : }
253 53 : or.rw.markStart(c);
254 53 : }
255 53 : for (RevCommit c : or.rw) {
256 53 : String name = c.name();
257 53 : if (ignoreHashes.contains(name)) {
258 0 : continue;
259 : }
260 53 : if (destHashes.size() < limit) {
261 53 : destHashes.add(name);
262 : } else {
263 : break;
264 : }
265 53 : }
266 :
267 53 : return destHashes;
268 : }
269 :
270 : private void markHeadUninteresting(OpenRepo or, BranchNameKey b) throws IOException {
271 53 : Optional<RevCommit> head = heads.get(b);
272 53 : if (head == null) {
273 53 : Ref ref = or.repo.getRefDatabase().exactRef(b.branch());
274 53 : head = ref != null ? Optional.of(or.rw.parseCommit(ref.getObjectId())) : Optional.empty();
275 53 : heads.put(b, head);
276 : }
277 53 : if (head.isPresent()) {
278 53 : or.rw.markUninteresting(head.get());
279 : }
280 53 : }
281 :
282 : private void logErrorAndThrow(String msg) {
283 0 : logger.atSevere().log("%s", msg);
284 0 : throw new StorageException(msg);
285 : }
286 : }
|