Line data Source code
1 : // Copyright (C) 2012 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.change;
16 :
17 : import static com.google.common.collect.ImmutableMap.toImmutableMap;
18 : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
19 : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
20 : import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
21 : import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
22 : import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
23 : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
24 : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
25 : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
26 : import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
27 : import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
28 : import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
29 : import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
30 : import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
31 : import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
32 : import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_DIFFSTAT;
33 : import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
34 : import static com.google.gerrit.extensions.client.ListChangesOption.SUBMIT_REQUIREMENTS;
35 : import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
36 : import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
37 : import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
38 : import static com.google.gerrit.server.util.AttentionSetUtil.removalsOnly;
39 : import static java.util.stream.Collectors.toList;
40 :
41 : import com.google.common.base.Joiner;
42 : import com.google.common.base.MoreObjects;
43 : import com.google.common.base.Throwables;
44 : import com.google.common.collect.ImmutableList;
45 : import com.google.common.collect.ImmutableListMultimap;
46 : import com.google.common.collect.ImmutableMap;
47 : import com.google.common.collect.ImmutableSet;
48 : import com.google.common.collect.ImmutableSortedMap;
49 : import com.google.common.collect.ListMultimap;
50 : import com.google.common.collect.Lists;
51 : import com.google.common.collect.Maps;
52 : import com.google.common.collect.Sets;
53 : import com.google.common.flogger.FluentLogger;
54 : import com.google.gerrit.common.Nullable;
55 : import com.google.gerrit.entities.Account;
56 : import com.google.gerrit.entities.Address;
57 : import com.google.gerrit.entities.Change;
58 : import com.google.gerrit.entities.ChangeMessage;
59 : import com.google.gerrit.entities.LegacySubmitRequirement;
60 : import com.google.gerrit.entities.PatchSet;
61 : import com.google.gerrit.entities.PatchSetApproval;
62 : import com.google.gerrit.entities.Project;
63 : import com.google.gerrit.entities.RefNames;
64 : import com.google.gerrit.entities.SubmitRecord;
65 : import com.google.gerrit.entities.SubmitRecord.Status;
66 : import com.google.gerrit.entities.SubmitRequirementResult;
67 : import com.google.gerrit.entities.SubmitTypeRecord;
68 : import com.google.gerrit.exceptions.StorageException;
69 : import com.google.gerrit.extensions.api.changes.FixInput;
70 : import com.google.gerrit.extensions.client.ListChangesOption;
71 : import com.google.gerrit.extensions.client.ReviewerState;
72 : import com.google.gerrit.extensions.common.AccountInfo;
73 : import com.google.gerrit.extensions.common.ApprovalInfo;
74 : import com.google.gerrit.extensions.common.ChangeInfo;
75 : import com.google.gerrit.extensions.common.ChangeMessageInfo;
76 : import com.google.gerrit.extensions.common.LabelInfo;
77 : import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
78 : import com.google.gerrit.extensions.common.PluginDefinedInfo;
79 : import com.google.gerrit.extensions.common.ProblemInfo;
80 : import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
81 : import com.google.gerrit.extensions.common.RevisionInfo;
82 : import com.google.gerrit.extensions.common.SubmitRecordInfo;
83 : import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
84 : import com.google.gerrit.extensions.common.TrackingIdInfo;
85 : import com.google.gerrit.extensions.restapi.Url;
86 : import com.google.gerrit.index.RefState;
87 : import com.google.gerrit.index.query.QueryResult;
88 : import com.google.gerrit.metrics.Description;
89 : import com.google.gerrit.metrics.Description.Units;
90 : import com.google.gerrit.metrics.MetricMaker;
91 : import com.google.gerrit.metrics.Timer0;
92 : import com.google.gerrit.server.ChangeMessagesUtil;
93 : import com.google.gerrit.server.CurrentUser;
94 : import com.google.gerrit.server.GpgException;
95 : import com.google.gerrit.server.ReviewerByEmailSet;
96 : import com.google.gerrit.server.ReviewerSet;
97 : import com.google.gerrit.server.ReviewerStatusUpdate;
98 : import com.google.gerrit.server.StarredChangesUtil;
99 : import com.google.gerrit.server.account.AccountInfoComparator;
100 : import com.google.gerrit.server.account.AccountLoader;
101 : import com.google.gerrit.server.cancellation.RequestCancelledException;
102 : import com.google.gerrit.server.config.GerritServerConfig;
103 : import com.google.gerrit.server.config.TrackingFooters;
104 : import com.google.gerrit.server.index.change.ChangeField;
105 : import com.google.gerrit.server.notedb.ChangeNotes;
106 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
107 : import com.google.gerrit.server.patch.PatchListNotAvailableException;
108 : import com.google.gerrit.server.permissions.ChangePermission;
109 : import com.google.gerrit.server.permissions.PermissionBackend;
110 : import com.google.gerrit.server.permissions.PermissionBackendException;
111 : import com.google.gerrit.server.project.RemoveReviewerControl;
112 : import com.google.gerrit.server.project.SubmitRuleOptions;
113 : import com.google.gerrit.server.query.change.ChangeData;
114 : import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
115 : import com.google.gerrit.server.util.AttentionSetUtil;
116 : import com.google.inject.Inject;
117 : import com.google.inject.Provider;
118 : import com.google.inject.Singleton;
119 : import com.google.inject.assistedinject.Assisted;
120 : import java.io.IOException;
121 : import java.util.ArrayList;
122 : import java.util.Collection;
123 : import java.util.Collections;
124 : import java.util.HashMap;
125 : import java.util.HashSet;
126 : import java.util.List;
127 : import java.util.Map;
128 : import java.util.Optional;
129 : import java.util.Set;
130 : import java.util.stream.Collectors;
131 : import org.eclipse.jgit.lib.Config;
132 : import org.eclipse.jgit.lib.ObjectId;
133 :
134 : /**
135 : * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
136 : *
137 : * <p>This is intended to be used on request scope, but may be used for converting multiple {@link
138 : * ChangeData} objects from different sources.
139 : */
140 : public class ChangeJson {
141 103 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
142 :
143 103 : public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
144 103 : ChangeField.SUBMIT_RULE_OPTIONS_LENIENT.toBuilder().build();
145 :
146 103 : public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
147 103 : ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build();
148 :
149 103 : static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
150 103 : ImmutableSet.of(
151 : ALL_COMMITS,
152 : ALL_REVISIONS,
153 : CHANGE_ACTIONS,
154 : CHECK,
155 : COMMIT_FOOTERS,
156 : CURRENT_ACTIONS,
157 : CURRENT_COMMIT,
158 : MESSAGES);
159 :
160 : @Singleton
161 : public static class Factory {
162 : private final AssistedFactory factory;
163 :
164 : @Inject
165 151 : Factory(AssistedFactory factory) {
166 151 : this.factory = factory;
167 151 : }
168 :
169 : public ChangeJson noOptions() {
170 60 : return create(ImmutableSet.of());
171 : }
172 :
173 : public ChangeJson create(Iterable<ListChangesOption> options) {
174 103 : return factory.create(options, Optional.empty());
175 : }
176 :
177 : public ChangeJson create(
178 : Iterable<ListChangesOption> options, PluginDefinedInfosFactory pluginDefinedInfosFactory) {
179 70 : return factory.create(options, Optional.of(pluginDefinedInfosFactory));
180 : }
181 :
182 : public ChangeJson create(ListChangesOption first, ListChangesOption... rest) {
183 5 : return create(Sets.immutableEnumSet(first, rest));
184 : }
185 : }
186 :
187 : public interface AssistedFactory {
188 : ChangeJson create(
189 : Iterable<ListChangesOption> options,
190 : Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory);
191 : }
192 :
193 : @Singleton
194 : private static class Metrics {
195 : private final Timer0 toChangeInfoLatency;
196 : private final Timer0 toChangeInfosLatency;
197 : private final Timer0 formatQueryResultsLatency;
198 :
199 : @Inject
200 103 : Metrics(MetricMaker metricMaker) {
201 103 : toChangeInfoLatency =
202 103 : metricMaker.newTimer(
203 : "http/server/rest_api/change_json/to_change_info_latency",
204 : new Description("Latency for toChangeInfo invocations in ChangeJson")
205 103 : .setCumulative()
206 103 : .setUnit(Units.MILLISECONDS));
207 103 : toChangeInfosLatency =
208 103 : metricMaker.newTimer(
209 : "http/server/rest_api/change_json/to_change_infos_latency",
210 : new Description("Latency for toChangeInfos invocations in ChangeJson")
211 103 : .setCumulative()
212 103 : .setUnit(Units.MILLISECONDS));
213 103 : formatQueryResultsLatency =
214 103 : metricMaker.newTimer(
215 : "http/server/rest_api/change_json/format_query_results_latency",
216 : new Description("Latency for formatQueryResults invocations in ChangeJson")
217 103 : .setCumulative()
218 103 : .setUnit(Units.MILLISECONDS));
219 103 : }
220 : }
221 :
222 : private final Provider<CurrentUser> userProvider;
223 : private final PermissionBackend permissionBackend;
224 : private final ChangeData.Factory changeDataFactory;
225 : private final AccountLoader.Factory accountLoaderFactory;
226 : private final ImmutableSet<ListChangesOption> options;
227 : private final ChangeMessagesUtil cmUtil;
228 : private final Provider<ConsistencyChecker> checkerProvider;
229 : private final ActionJson actionJson;
230 : private final ChangeNotes.Factory notesFactory;
231 : private final LabelsJson labelsJson;
232 : private final RemoveReviewerControl removeReviewerControl;
233 : private final TrackingFooters trackingFooters;
234 : private final Metrics metrics;
235 : private final RevisionJson revisionJson;
236 : private final Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory;
237 : private final boolean includeMergeable;
238 : private final boolean lazyLoad;
239 : private final boolean cacheQueryResultsByChangeNum;
240 :
241 : private AccountLoader accountLoader;
242 : private FixInput fix;
243 :
244 : @Inject
245 : ChangeJson(
246 : Provider<CurrentUser> user,
247 : PermissionBackend permissionBackend,
248 : ChangeData.Factory cdf,
249 : AccountLoader.Factory ailf,
250 : ChangeMessagesUtil cmUtil,
251 : Provider<ConsistencyChecker> checkerProvider,
252 : ActionJson actionJson,
253 : ChangeNotes.Factory notesFactory,
254 : LabelsJson labelsJson,
255 : RemoveReviewerControl removeReviewerControl,
256 : TrackingFooters trackingFooters,
257 : Metrics metrics,
258 : RevisionJson.Factory revisionJsonFactory,
259 : @GerritServerConfig Config cfg,
260 : @Assisted Iterable<ListChangesOption> options,
261 103 : @Assisted Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory) {
262 103 : this.userProvider = user;
263 103 : this.changeDataFactory = cdf;
264 103 : this.permissionBackend = permissionBackend;
265 103 : this.accountLoaderFactory = ailf;
266 103 : this.cmUtil = cmUtil;
267 103 : this.checkerProvider = checkerProvider;
268 103 : this.actionJson = actionJson;
269 103 : this.notesFactory = notesFactory;
270 103 : this.labelsJson = labelsJson;
271 103 : this.removeReviewerControl = removeReviewerControl;
272 103 : this.trackingFooters = trackingFooters;
273 103 : this.metrics = metrics;
274 103 : this.revisionJson = revisionJsonFactory.create(options);
275 103 : this.options = Sets.immutableEnumSet(options);
276 103 : this.includeMergeable = MergeabilityComputationBehavior.fromConfig(cfg).includeInApi();
277 103 : this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD);
278 103 : this.pluginDefinedInfosFactory = pluginDefinedInfosFactory;
279 103 : this.cacheQueryResultsByChangeNum =
280 103 : cfg.getBoolean("index", "cacheQueryResultsByChangeNum", true);
281 :
282 103 : logger.atFine().log("options = %s", options);
283 103 : }
284 :
285 : public ChangeJson fix(FixInput fix) {
286 3 : this.fix = fix;
287 3 : return this;
288 : }
289 :
290 : public ChangeInfo format(ChangeResource rsrc) {
291 2 : return format(changeDataFactory.create(rsrc.getNotes()));
292 : }
293 :
294 : public ChangeInfo format(Change change) {
295 60 : return format(changeDataFactory.create(change));
296 : }
297 :
298 : public ChangeInfo format(Change change, @Nullable ObjectId metaRevId) {
299 67 : ChangeNotes notes = notesFactory.createChecked(change.getProject(), change.getId(), metaRevId);
300 67 : return format(changeDataFactory.create(notes));
301 : }
302 :
303 : public ChangeInfo format(ChangeData cd) {
304 103 : return format(cd, Optional.empty(), true, getPluginInfos(cd));
305 : }
306 :
307 : public ChangeInfo format(RevisionResource rsrc) {
308 3 : ChangeData cd = changeDataFactory.create(rsrc.getNotes());
309 3 : return format(cd, Optional.of(rsrc.getPatchSet().id()), true, getPluginInfos(cd));
310 : }
311 :
312 : public List<List<ChangeInfo>> format(List<QueryResult<ChangeData>> in)
313 : throws PermissionBackendException {
314 26 : try (Timer0.Context ignored = metrics.formatQueryResultsLatency.start()) {
315 26 : accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
316 26 : List<List<ChangeInfo>> res = new ArrayList<>(in.size());
317 26 : Map<Change.Id, ChangeInfo> cache = Maps.newHashMapWithExpectedSize(in.size());
318 26 : ImmutableListMultimap<Change.Id, PluginDefinedInfo> pluginInfosByChange =
319 26 : getPluginInfos(in.stream().flatMap(e -> e.entities().stream()).collect(toList()));
320 26 : for (QueryResult<ChangeData> r : in) {
321 26 : List<ChangeInfo> infos = toChangeInfos(r.entities(), cache, pluginInfosByChange);
322 26 : if (!infos.isEmpty() && r.more()) {
323 6 : infos.get(infos.size() - 1)._moreChanges = true;
324 : }
325 26 : res.add(infos);
326 26 : }
327 26 : accountLoader.fill();
328 26 : return res;
329 : }
330 : }
331 :
332 : public List<ChangeInfo> format(Collection<ChangeData> in) throws PermissionBackendException {
333 11 : accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
334 11 : ensureLoaded(in);
335 11 : List<ChangeInfo> out = new ArrayList<>(in.size());
336 11 : ImmutableListMultimap<Change.Id, PluginDefinedInfo> pluginInfosByChange = getPluginInfos(in);
337 11 : for (ChangeData cd : in) {
338 10 : out.add(format(cd, Optional.empty(), false, pluginInfosByChange.get(cd.getId())));
339 10 : }
340 11 : accountLoader.fill();
341 11 : return out;
342 : }
343 :
344 : public ChangeInfo format(Project.NameKey project, Change.Id id) {
345 25 : return format(project, id, null);
346 : }
347 :
348 : public ChangeInfo format(Project.NameKey project, Change.Id id, @Nullable ObjectId metaRevId) {
349 : ChangeNotes notes;
350 : try {
351 25 : notes = notesFactory.createChecked(project, id, metaRevId);
352 0 : } catch (StorageException e) {
353 0 : if (!has(CHECK)) {
354 0 : throw e;
355 : }
356 0 : return checkOnly(changeDataFactory.create(project, id));
357 25 : }
358 25 : ChangeData cd = changeDataFactory.create(notes);
359 25 : return format(cd, Optional.empty(), true, getPluginInfos(cd));
360 : }
361 :
362 : private static Collection<LegacySubmitRequirementInfo> requirementsFor(ChangeData cd) {
363 103 : Collection<LegacySubmitRequirementInfo> reqInfos = new ArrayList<>();
364 103 : for (SubmitRecord submitRecord : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
365 103 : if (submitRecord.requirements == null) {
366 103 : continue;
367 : }
368 3 : for (LegacySubmitRequirement requirement : submitRecord.requirements) {
369 3 : reqInfos.add(requirementToInfo(requirement, submitRecord.status));
370 3 : }
371 3 : }
372 103 : return reqInfos;
373 : }
374 :
375 : private Collection<SubmitRecordInfo> submitRecordsFor(ChangeData cd) {
376 103 : List<SubmitRecordInfo> submitRecordInfos = new ArrayList<>();
377 103 : for (SubmitRecord record : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
378 103 : submitRecordInfos.add(submitRecordToInfo(record));
379 103 : }
380 103 : return submitRecordInfos;
381 : }
382 :
383 : private Collection<SubmitRequirementResultInfo> submitRequirementsFor(ChangeData cd) {
384 103 : Collection<SubmitRequirementResultInfo> reqInfos = new ArrayList<>();
385 103 : cd.submitRequirementsIncludingLegacy().entrySet().stream()
386 103 : .filter(entry -> !entry.getValue().isHidden())
387 103 : .forEach(
388 103 : entry -> reqInfos.add(SubmitRequirementsJson.toInfo(entry.getKey(), entry.getValue())));
389 103 : return reqInfos;
390 : }
391 :
392 : private static LegacySubmitRequirementInfo requirementToInfo(
393 : LegacySubmitRequirement req, Status status) {
394 3 : return new LegacySubmitRequirementInfo(status.name(), req.fallbackText(), req.type());
395 : }
396 :
397 : private SubmitRecordInfo submitRecordToInfo(SubmitRecord record) {
398 103 : SubmitRecordInfo info = new SubmitRecordInfo();
399 103 : if (record.status != null) {
400 103 : info.status = SubmitRecordInfo.Status.valueOf(record.status.name());
401 : }
402 103 : info.ruleName = record.ruleName;
403 103 : info.errorMessage = record.errorMessage;
404 103 : if (record.labels != null) {
405 103 : info.labels = new ArrayList<>();
406 103 : for (SubmitRecord.Label label : record.labels) {
407 103 : SubmitRecordInfo.Label labelInfo = new SubmitRecordInfo.Label();
408 103 : labelInfo.label = label.label;
409 103 : if (label.status != null) {
410 103 : labelInfo.status = SubmitRecordInfo.Label.Status.valueOf(label.status.name());
411 : }
412 103 : labelInfo.appliedBy = accountLoader.get(label.appliedBy);
413 103 : info.labels.add(labelInfo);
414 103 : }
415 : }
416 103 : if (record.requirements != null) {
417 3 : info.requirements = new ArrayList<>();
418 3 : for (LegacySubmitRequirement requirement : record.requirements) {
419 3 : info.requirements.add(requirementToInfo(requirement, record.status));
420 3 : }
421 : }
422 103 : return info;
423 : }
424 :
425 : private static void finish(ChangeInfo info) {
426 103 : info.id =
427 103 : Joiner.on('~')
428 103 : .join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId));
429 103 : }
430 :
431 : private static boolean containsAnyOf(
432 : ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
433 103 : return !Sets.intersection(toFind, set).isEmpty();
434 : }
435 :
436 : private ChangeInfo format(
437 : ChangeData cd,
438 : Optional<PatchSet.Id> limitToPsId,
439 : boolean fillAccountLoader,
440 : List<PluginDefinedInfo> pluginInfosForChange) {
441 : try {
442 103 : if (fillAccountLoader) {
443 103 : accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
444 103 : ChangeInfo res = toChangeInfo(cd, limitToPsId, pluginInfosForChange);
445 103 : accountLoader.fill();
446 103 : return res;
447 : }
448 32 : return toChangeInfo(cd, limitToPsId, pluginInfosForChange);
449 1 : } catch (PatchListNotAvailableException
450 : | GpgException
451 : | IOException
452 : | PermissionBackendException
453 : | RuntimeException e) {
454 1 : if (!has(CHECK)) {
455 1 : Throwables.throwIfInstanceOf(e, StorageException.class);
456 1 : throw new StorageException(e);
457 : }
458 0 : return checkOnly(cd);
459 : }
460 : }
461 :
462 : private void ensureLoaded(Iterable<ChangeData> all) {
463 33 : if (lazyLoad) {
464 3 : for (ChangeData cd : all) {
465 : // Mark all ChangeDatas as coming from the index, but allow backfilling data from NoteDb
466 3 : cd.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
467 3 : }
468 3 : ChangeData.ensureChangeLoaded(all);
469 3 : if (has(ALL_REVISIONS)) {
470 3 : ChangeData.ensureAllPatchSetsLoaded(all);
471 1 : } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
472 1 : ChangeData.ensureCurrentPatchSetLoaded(all);
473 : }
474 3 : if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
475 0 : ChangeData.ensureReviewedByLoadedForOpenChanges(all);
476 : }
477 3 : ChangeData.ensureCurrentApprovalsLoaded(all);
478 : } else {
479 33 : for (ChangeData cd : all) {
480 : // Mark all ChangeDatas as coming from the index. Disallow using NoteDb
481 32 : cd.setStorageConstraint(ChangeData.StorageConstraint.INDEX_ONLY);
482 32 : }
483 : }
484 33 : }
485 :
486 : private boolean has(ListChangesOption option) {
487 103 : return options.contains(option);
488 : }
489 :
490 : private List<ChangeInfo> toChangeInfos(
491 : List<ChangeData> changes,
492 : Map<Change.Id, ChangeInfo> cache,
493 : ImmutableListMultimap<Change.Id, PluginDefinedInfo> pluginInfosByChange) {
494 26 : try (Timer0.Context ignored = metrics.toChangeInfosLatency.start()) {
495 26 : List<ChangeInfo> changeInfos = new ArrayList<>(changes.size());
496 26 : for (int i = 0; i < changes.size(); i++) {
497 : // We can only cache and re-use an entity if it's not the last in the list. The last entity
498 : // may later get _moreChanges set. If it was cached or re-used, that setting would propagate
499 : // to the original entity yielding wrong results.
500 : // This problem has two sides where 'last in the list' has to be respected:
501 : // (1) Caching
502 : // (2) Reusing
503 25 : boolean isCacheable = cacheQueryResultsByChangeNum && (i != changes.size() - 1);
504 25 : ChangeData cd = changes.get(i);
505 25 : ChangeInfo info = cache.get(cd.getId());
506 25 : if (info != null && isCacheable) {
507 1 : changeInfos.add(info);
508 1 : continue;
509 : }
510 :
511 : // Compute and cache if possible
512 : try {
513 25 : ensureLoaded(Collections.singleton(cd));
514 25 : info = format(cd, Optional.empty(), false, pluginInfosByChange.get(cd.getId()));
515 25 : changeInfos.add(info);
516 25 : if (isCacheable) {
517 16 : cache.put(Change.id(info._number), info);
518 : }
519 0 : } catch (RuntimeException e) {
520 0 : Optional<RequestCancelledException> requestCancelledException =
521 0 : RequestCancelledException.getFromCausalChain(e);
522 0 : if (requestCancelledException.isPresent()) {
523 0 : throw e;
524 : }
525 0 : logger.atWarning().withCause(e).log(
526 0 : "Omitting corrupt change %s from results", cd.getId());
527 25 : }
528 : }
529 26 : return changeInfos;
530 : }
531 : }
532 :
533 : private ChangeInfo checkOnly(ChangeData cd) {
534 : ChangeNotes notes;
535 : try {
536 0 : notes = cd.notes();
537 0 : } catch (StorageException e) {
538 0 : String msg = "Error loading change";
539 0 : logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
540 0 : ChangeInfo info = new ChangeInfo();
541 0 : info._number = cd.getId().get();
542 0 : ProblemInfo p = new ProblemInfo();
543 0 : p.message = msg;
544 0 : info.problems = Lists.newArrayList(p);
545 0 : return info;
546 0 : }
547 :
548 0 : ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix);
549 0 : ChangeInfo info = new ChangeInfo();
550 0 : Change c = result.change();
551 0 : if (c != null) {
552 0 : info.project = c.getProject().get();
553 0 : info.branch = c.getDest().shortName();
554 0 : info.topic = c.getTopic();
555 0 : info.changeId = c.getKey().get();
556 0 : info.subject = c.getSubject();
557 0 : info.status = c.getStatus().asChangeStatus();
558 0 : info.owner = new AccountInfo(c.getOwner().get());
559 0 : info.setCreated(c.getCreatedOn());
560 0 : info.setUpdated(c.getLastUpdatedOn());
561 0 : info._number = c.getId().get();
562 0 : info.problems = result.problems();
563 0 : info.isPrivate = c.isPrivate() ? true : null;
564 0 : info.workInProgress = c.isWorkInProgress() ? true : null;
565 0 : info.hasReviewStarted = c.hasReviewStarted();
566 0 : finish(info);
567 : } else {
568 0 : info._number = result.id().get();
569 0 : info.problems = result.problems();
570 : }
571 0 : return info;
572 : }
573 :
574 : private ChangeInfo toChangeInfo(
575 : ChangeData cd,
576 : Optional<PatchSet.Id> limitToPsId,
577 : List<PluginDefinedInfo> pluginInfosForChange)
578 : throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
579 103 : try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
580 103 : return toChangeInfoImpl(cd, limitToPsId, pluginInfosForChange);
581 : }
582 : }
583 :
584 : private ChangeInfo toChangeInfoImpl(
585 : ChangeData cd, Optional<PatchSet.Id> limitToPsId, List<PluginDefinedInfo> pluginInfos)
586 : throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
587 103 : ChangeInfo out = new ChangeInfo();
588 103 : CurrentUser user = userProvider.get();
589 :
590 103 : if (has(CHECK)) {
591 5 : out.problems = checkerProvider.get().check(cd.notes(), fix).problems();
592 : // If any problems were fixed, the ChangeData needs to be reloaded.
593 5 : for (ProblemInfo p : out.problems) {
594 2 : if (p.status == ProblemInfo.Status.FIXED) {
595 2 : cd = changeDataFactory.create(cd.project(), cd.getId());
596 2 : break;
597 : }
598 1 : }
599 : }
600 :
601 103 : Change in = cd.change();
602 103 : out.project = in.getProject().get();
603 103 : out.branch = in.getDest().shortName();
604 103 : out.topic = in.getTopic();
605 103 : if (!cd.attentionSet().isEmpty()) {
606 49 : out.removedFromAttentionSet =
607 49 : removalsOnly(cd.attentionSet()).stream()
608 49 : .collect(
609 49 : toImmutableMap(
610 39 : a -> a.account().get(),
611 39 : a -> AttentionSetUtil.createAttentionSetInfo(a, accountLoader)));
612 49 : out.attentionSet =
613 : // This filtering should match GetAttentionSet.
614 49 : additionsOnly(cd.attentionSet()).stream()
615 49 : .collect(
616 49 : toImmutableMap(
617 49 : a -> a.account().get(),
618 49 : a -> AttentionSetUtil.createAttentionSetInfo(a, accountLoader)));
619 : }
620 103 : out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null;
621 103 : out.hashtags = cd.hashtags();
622 103 : out.changeId = in.getKey().get();
623 103 : if (in.isNew()) {
624 103 : SubmitTypeRecord str = cd.submitTypeRecord();
625 103 : if (str.isOk()) {
626 103 : out.submitType = str.type;
627 : }
628 103 : if (includeMergeable) {
629 14 : out.mergeable = cd.isMergeable();
630 : }
631 103 : if (has(SUBMITTABLE)) {
632 103 : out.submittable = submittable(cd);
633 : }
634 : }
635 103 : if (!has(SKIP_DIFFSTAT)) {
636 81 : Optional<ChangedLines> changedLines = cd.changedLines();
637 81 : if (changedLines.isPresent()) {
638 81 : out.insertions = changedLines.get().insertions;
639 81 : out.deletions = changedLines.get().deletions;
640 : }
641 : }
642 103 : out.isPrivate = in.isPrivate() ? true : null;
643 103 : out.workInProgress = in.isWorkInProgress() ? true : null;
644 103 : out.hasReviewStarted = in.hasReviewStarted();
645 103 : out.subject = in.getSubject();
646 103 : out.status = in.getStatus().asChangeStatus();
647 103 : out.owner = accountLoader.get(in.getOwner());
648 103 : out.setCreated(in.getCreatedOn());
649 103 : out.setUpdated(in.getLastUpdatedOn());
650 103 : out._number = in.getId().get();
651 103 : out.totalCommentCount = cd.totalCommentCount();
652 103 : out.unresolvedCommentCount = cd.unresolvedCommentCount();
653 :
654 103 : if (cd.getRefStates() != null) {
655 103 : String metaName = RefNames.changeMetaRef(cd.getId());
656 103 : Optional<RefState> metaState =
657 103 : cd.getRefStates().values().stream().filter(r -> r.ref().equals(metaName)).findAny();
658 :
659 : // metaState should always be there, but it doesn't hurt to be extra careful.
660 103 : metaState.ifPresent(rs -> out.metaRevId = rs.id().getName());
661 : }
662 :
663 103 : if (user.isIdentifiedUser()) {
664 103 : Collection<String> stars = cd.stars(user.getAccountId());
665 103 : out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
666 103 : if (!stars.isEmpty()) {
667 6 : out.stars = stars;
668 : }
669 : }
670 :
671 103 : if (in.isNew() && has(REVIEWED) && user.isIdentifiedUser()) {
672 103 : out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
673 : }
674 :
675 103 : out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS));
676 103 : out.requirements = requirementsFor(cd);
677 103 : out.submitRecords = submitRecordsFor(cd);
678 103 : if (has(SUBMIT_REQUIREMENTS)) {
679 103 : out.submitRequirements = submitRequirementsFor(cd);
680 : }
681 :
682 103 : if (out.labels != null && has(DETAILED_LABELS)) {
683 : // If limited to specific patch sets but not the current patch set, don't
684 : // list permitted labels, since users can't vote on those patch sets.
685 103 : if (user.isIdentifiedUser()
686 103 : && (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) {
687 103 : out.permittedLabels =
688 103 : !cd.change().isAbandoned()
689 103 : ? labelsJson.permittedLabels(user.getAccountId(), cd)
690 103 : : ImmutableMap.of();
691 : }
692 : }
693 :
694 103 : if (has(LABELS) || has(DETAILED_LABELS)) {
695 103 : out.reviewers = reviewerMap(cd.reviewers(), cd.reviewersByEmail(), false);
696 103 : out.pendingReviewers = reviewerMap(cd.pendingReviewers(), cd.pendingReviewersByEmail(), true);
697 103 : out.removableReviewers = removableReviewers(cd, out);
698 : }
699 :
700 103 : setSubmitter(cd, out);
701 :
702 103 : if (!pluginInfos.isEmpty()) {
703 2 : out.plugins = pluginInfos;
704 : }
705 103 : out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null;
706 103 : out.submissionId = cd.change().getSubmissionId();
707 103 : out.cherryPickOfChange =
708 103 : cd.change().getCherryPickOf() != null
709 10 : ? cd.change().getCherryPickOf().changeId().get()
710 103 : : null;
711 103 : out.cherryPickOfPatchSet =
712 103 : cd.change().getCherryPickOf() != null ? cd.change().getCherryPickOf().get() : null;
713 :
714 103 : if (has(REVIEWER_UPDATES)) {
715 103 : out.reviewerUpdates = reviewerUpdates(cd);
716 : }
717 :
718 103 : boolean needMessages = has(MESSAGES);
719 103 : boolean needRevisions = has(ALL_REVISIONS) || has(CURRENT_REVISION) || limitToPsId.isPresent();
720 : Map<PatchSet.Id, PatchSet> src;
721 103 : if (needMessages || needRevisions) {
722 103 : src = loadPatchSets(cd, limitToPsId);
723 : } else {
724 69 : src = null;
725 : }
726 :
727 103 : if (needMessages) {
728 103 : out.messages = messages(cd);
729 : }
730 103 : finish(out);
731 :
732 : // This block must come after the ChangeInfo is mostly populated, since
733 : // it will be passed to ActionVisitors as-is.
734 103 : if (needRevisions) {
735 103 : out.revisions = revisionJson.getRevisions(accountLoader, cd, src, limitToPsId, out);
736 103 : if (out.revisions != null) {
737 103 : for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
738 103 : if (entry.getValue().isCurrent) {
739 103 : out.currentRevision = entry.getKey();
740 103 : break;
741 : }
742 52 : }
743 : }
744 : }
745 :
746 103 : if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) {
747 57 : actionJson.addChangeActions(out, cd);
748 : }
749 :
750 103 : if (has(TRACKING_IDS)) {
751 103 : ListMultimap<String, String> set = trackingFooters.extract(cd.commitFooters());
752 103 : out.trackingIds =
753 103 : set.entries().stream()
754 103 : .map(e -> new TrackingIdInfo(e.getKey(), e.getValue()))
755 103 : .collect(toList());
756 : }
757 :
758 103 : return out;
759 : }
760 :
761 : private Map<ReviewerState, Collection<AccountInfo>> reviewerMap(
762 : ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, boolean includeRemoved) {
763 103 : Map<ReviewerState, Collection<AccountInfo>> reviewerMap = new HashMap<>();
764 103 : for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
765 103 : if (!includeRemoved && state == ReviewerStateInternal.REMOVED) {
766 103 : continue;
767 : }
768 103 : Collection<AccountInfo> reviewersByState = toAccountInfo(reviewers.byState(state));
769 103 : reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state)));
770 103 : if (!reviewersByState.isEmpty()) {
771 72 : reviewerMap.put(state.asReviewerState(), reviewersByState);
772 : }
773 : }
774 103 : return reviewerMap;
775 : }
776 :
777 : private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) {
778 103 : List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
779 103 : List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
780 103 : for (ReviewerStatusUpdate c : reviewerUpdates) {
781 46 : ReviewerUpdateInfo change =
782 : new ReviewerUpdateInfo(
783 46 : c.date(),
784 46 : accountLoader.get(c.updatedBy()),
785 46 : accountLoader.get(c.reviewer()),
786 46 : c.state().asReviewerState());
787 46 : result.add(change);
788 46 : }
789 103 : return result;
790 : }
791 :
792 : private boolean submittable(ChangeData cd) {
793 103 : return cd.submitRequirementsIncludingLegacy().values().stream()
794 103 : .allMatch(SubmitRequirementResult::fulfilled);
795 : }
796 :
797 : private void setSubmitter(ChangeData cd, ChangeInfo out) {
798 103 : Optional<PatchSetApproval> s = cd.getSubmitApproval();
799 103 : if (!s.isPresent()) {
800 103 : return;
801 : }
802 55 : out.setSubmitted(s.get().granted(), accountLoader.get(s.get().accountId()));
803 55 : }
804 :
805 : private ImmutableList<ChangeMessageInfo> messages(ChangeData cd) {
806 103 : List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
807 103 : if (messages.isEmpty()) {
808 5 : return ImmutableList.of();
809 : }
810 :
811 102 : List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size());
812 102 : for (ChangeMessage message : messages) {
813 102 : result.add(createChangeMessageInfo(message, accountLoader));
814 102 : }
815 102 : return ImmutableList.copyOf(result);
816 : }
817 :
818 : private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
819 : throws PermissionBackendException {
820 : // Although this is called removableReviewers, this method also determines
821 : // which CCs are removable.
822 : //
823 : // For reviewers, we need to look at each approval, because the reviewer
824 : // should only be considered removable if *all* of their approvals can be
825 : // removed. First, add all reviewers with *any* removable approval to the
826 : // "removable" set. Along the way, if we encounter a non-removable approval,
827 : // add the reviewer to the "fixed" set. Before we return, remove all members
828 : // of "fixed" from "removable", because not all of their approvals can be
829 : // removed.
830 103 : Collection<LabelInfo> labels = out.labels.values();
831 103 : Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
832 103 : Set<Account.Id> removable = new HashSet<>();
833 :
834 : // Add all reviewers, which will later be removed if they are in the "fixed" set.
835 103 : removable.addAll(
836 103 : out.reviewers.getOrDefault(ReviewerState.REVIEWER, Collections.emptySet()).stream()
837 103 : .filter(a -> a._accountId != null)
838 103 : .map(a -> Account.id(a._accountId))
839 103 : .collect(Collectors.toSet()));
840 :
841 : // Check if the user has the permission to remove a reviewer. This means we can bypass the
842 : // testRemoveReviewer check for a specific reviewer in the loop saving potentially many
843 : // permission checks.
844 103 : boolean canRemoveAnyReviewer =
845 : permissionBackend
846 103 : .user(userProvider.get())
847 103 : .change(cd)
848 103 : .test(ChangePermission.REMOVE_REVIEWER);
849 103 : for (LabelInfo label : labels) {
850 103 : if (label.all == null) {
851 102 : continue;
852 : }
853 71 : for (ApprovalInfo ai : label.all) {
854 71 : Account.Id id = Account.id(ai._accountId);
855 :
856 71 : if (!canRemoveAnyReviewer
857 71 : && !removeReviewerControl.testRemoveReviewer(
858 71 : cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) {
859 49 : fixed.add(id);
860 : }
861 71 : }
862 71 : }
863 :
864 : // CCs are simpler than reviewers. They are removable if the ChangeControl
865 : // would permit a non-negative approval by that account to be removed, in
866 : // which case add them to removable. We don't need to add unremovable CCs to
867 : // "fixed" because we only visit each CC once here.
868 103 : Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
869 103 : if (ccs != null) {
870 25 : for (AccountInfo ai : ccs) {
871 25 : if (ai._accountId != null) {
872 25 : Account.Id id = Account.id(ai._accountId);
873 25 : if (canRemoveAnyReviewer
874 25 : || removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) {
875 25 : removable.add(id);
876 : }
877 : }
878 25 : }
879 : }
880 :
881 : // Subtract any reviewers with non-removable approvals from the "removable"
882 : // set. This also subtracts any CCs that for some reason also hold
883 : // unremovable approvals.
884 103 : removable.removeAll(fixed);
885 :
886 103 : List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size());
887 103 : for (Account.Id id : removable) {
888 72 : result.add(accountLoader.get(id));
889 72 : }
890 : // Reviewers added by email are always removable
891 103 : for (Collection<AccountInfo> infos : out.reviewers.values()) {
892 72 : for (AccountInfo info : infos) {
893 72 : if (info._accountId == null) {
894 10 : result.add(info);
895 : }
896 72 : }
897 72 : }
898 103 : return result;
899 : }
900 :
901 : private Collection<AccountInfo> toAccountInfo(Collection<Account.Id> accounts) {
902 103 : return accounts.stream()
903 103 : .map(accountLoader::get)
904 103 : .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
905 103 : .collect(toList());
906 : }
907 :
908 : private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
909 103 : return addresses.stream()
910 103 : .map(a -> new AccountInfo(a.name(), a.email()))
911 103 : .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
912 103 : .collect(toList());
913 : }
914 :
915 : private Map<PatchSet.Id, PatchSet> loadPatchSets(
916 : ChangeData cd, Optional<PatchSet.Id> limitToPsId) {
917 : Collection<PatchSet> src;
918 103 : if (has(ALL_REVISIONS) || has(MESSAGES)) {
919 103 : src = cd.patchSets();
920 : } else {
921 : PatchSet ps;
922 28 : if (limitToPsId.isPresent()) {
923 3 : ps = cd.patchSet(limitToPsId.get());
924 3 : if (ps == null) {
925 0 : throw new StorageException("missing patch set " + limitToPsId.get());
926 : }
927 : } else {
928 26 : ps = cd.currentPatchSet();
929 26 : if (ps == null) {
930 0 : throw new StorageException("missing current patch set for change " + cd.getId());
931 : }
932 : }
933 28 : src = Collections.singletonList(ps);
934 : }
935 : // Sort by patch set ID in increasing order to have a stable output.
936 103 : ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> map = ImmutableSortedMap.naturalOrder();
937 103 : for (PatchSet patchSet : src) {
938 103 : map.put(patchSet.id(), patchSet);
939 103 : }
940 103 : return map.build();
941 : }
942 :
943 : private List<PluginDefinedInfo> getPluginInfos(ChangeData cd) {
944 103 : return getPluginInfos(Collections.singleton(cd)).get(cd.getId());
945 : }
946 :
947 : private ImmutableListMultimap<Change.Id, PluginDefinedInfo> getPluginInfos(
948 : Collection<ChangeData> cds) {
949 103 : if (pluginDefinedInfosFactory.isPresent()) {
950 70 : return pluginDefinedInfosFactory.get().createPluginDefinedInfos(cds);
951 : }
952 103 : return ImmutableListMultimap.of();
953 : }
954 : }
|