Line data Source code
1 : // Copyright (C) 2009 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.query.change;
16 :
17 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
18 : import static java.util.Objects.requireNonNull;
19 : import static java.util.stream.Collectors.toList;
20 : import static java.util.stream.Collectors.toMap;
21 :
22 : import com.google.auto.value.AutoValue;
23 : import com.google.common.annotations.VisibleForTesting;
24 : import com.google.common.base.MoreObjects;
25 : import com.google.common.collect.HashBasedTable;
26 : import com.google.common.collect.ImmutableList;
27 : import com.google.common.collect.ImmutableListMultimap;
28 : import com.google.common.collect.ImmutableMap;
29 : import com.google.common.collect.ImmutableSet;
30 : import com.google.common.collect.ImmutableSetMultimap;
31 : import com.google.common.collect.ImmutableSortedSet;
32 : import com.google.common.collect.Iterables;
33 : import com.google.common.collect.ListMultimap;
34 : import com.google.common.collect.Lists;
35 : import com.google.common.collect.Maps;
36 : import com.google.common.collect.SetMultimap;
37 : import com.google.common.collect.Table;
38 : import com.google.common.flogger.FluentLogger;
39 : import com.google.common.primitives.Ints;
40 : import com.google.gerrit.common.Nullable;
41 : import com.google.gerrit.entities.Account;
42 : import com.google.gerrit.entities.AttentionSetUpdate;
43 : import com.google.gerrit.entities.Change;
44 : import com.google.gerrit.entities.ChangeMessage;
45 : import com.google.gerrit.entities.Comment;
46 : import com.google.gerrit.entities.HumanComment;
47 : import com.google.gerrit.entities.LabelTypes;
48 : import com.google.gerrit.entities.PatchSet;
49 : import com.google.gerrit.entities.PatchSetApproval;
50 : import com.google.gerrit.entities.Project;
51 : import com.google.gerrit.entities.Project.NameKey;
52 : import com.google.gerrit.entities.RefNames;
53 : import com.google.gerrit.entities.RobotComment;
54 : import com.google.gerrit.entities.SubmitRecord;
55 : import com.google.gerrit.entities.SubmitRequirement;
56 : import com.google.gerrit.entities.SubmitRequirementResult;
57 : import com.google.gerrit.entities.SubmitTypeRecord;
58 : import com.google.gerrit.exceptions.StorageException;
59 : import com.google.gerrit.extensions.restapi.BadRequestException;
60 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
61 : import com.google.gerrit.index.RefState;
62 : import com.google.gerrit.server.ChangeMessagesUtil;
63 : import com.google.gerrit.server.CommentsUtil;
64 : import com.google.gerrit.server.CurrentUser;
65 : import com.google.gerrit.server.PatchSetUtil;
66 : import com.google.gerrit.server.ReviewerByEmailSet;
67 : import com.google.gerrit.server.ReviewerSet;
68 : import com.google.gerrit.server.ReviewerStatusUpdate;
69 : import com.google.gerrit.server.StarredChangesUtil;
70 : import com.google.gerrit.server.StarredChangesUtil.StarRef;
71 : import com.google.gerrit.server.approval.ApprovalsUtil;
72 : import com.google.gerrit.server.change.CommentThread;
73 : import com.google.gerrit.server.change.CommentThreads;
74 : import com.google.gerrit.server.change.MergeabilityCache;
75 : import com.google.gerrit.server.change.PureRevert;
76 : import com.google.gerrit.server.config.AllUsersName;
77 : import com.google.gerrit.server.config.TrackingFooters;
78 : import com.google.gerrit.server.git.GitRepositoryManager;
79 : import com.google.gerrit.server.git.MergeUtilFactory;
80 : import com.google.gerrit.server.notedb.ChangeNotes;
81 : import com.google.gerrit.server.notedb.RobotCommentNotes;
82 : import com.google.gerrit.server.patch.DiffSummary;
83 : import com.google.gerrit.server.patch.DiffSummaryKey;
84 : import com.google.gerrit.server.patch.PatchListCache;
85 : import com.google.gerrit.server.patch.PatchListKey;
86 : import com.google.gerrit.server.patch.PatchListNotAvailableException;
87 : import com.google.gerrit.server.project.NoSuchChangeException;
88 : import com.google.gerrit.server.project.ProjectCache;
89 : import com.google.gerrit.server.project.ProjectState;
90 : import com.google.gerrit.server.project.SubmitRequirementsAdapter;
91 : import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
92 : import com.google.gerrit.server.project.SubmitRequirementsUtil;
93 : import com.google.gerrit.server.project.SubmitRuleEvaluator;
94 : import com.google.gerrit.server.project.SubmitRuleOptions;
95 : import com.google.gerrit.server.util.time.TimeUtil;
96 : import com.google.inject.Inject;
97 : import com.google.inject.assistedinject.Assisted;
98 : import java.io.IOException;
99 : import java.time.Instant;
100 : import java.util.ArrayList;
101 : import java.util.Collection;
102 : import java.util.Collections;
103 : import java.util.HashMap;
104 : import java.util.LinkedHashSet;
105 : import java.util.List;
106 : import java.util.Map;
107 : import java.util.Optional;
108 : import java.util.Set;
109 : import java.util.function.Function;
110 : import java.util.stream.Collectors;
111 : import java.util.stream.Stream;
112 : import org.eclipse.jgit.lib.ObjectId;
113 : import org.eclipse.jgit.lib.PersonIdent;
114 : import org.eclipse.jgit.lib.Ref;
115 : import org.eclipse.jgit.lib.Repository;
116 : import org.eclipse.jgit.revwalk.FooterLine;
117 : import org.eclipse.jgit.revwalk.RevCommit;
118 : import org.eclipse.jgit.revwalk.RevWalk;
119 :
120 : /**
121 : * ChangeData provides lazily loaded interface to change metadata loaded from NoteDb. It can be
122 : * constructed by loading from NoteDb, or calling setters. The latter happens when ChangeData is
123 : * retrieved through the change index. This happens for Applications that are performance sensitive
124 : * (eg. dashboard loads, git protocol negotiation) but can tolerate staleness. In that case, setting
125 : * lazyLoad=false disables loading from NoteDb, so we don't accidentally enable a slow path.
126 : */
127 : public class ChangeData {
128 154 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
129 :
130 104 : public enum StorageConstraint {
131 : /**
132 : * This instance was loaded from the change index. Backfilling missing data from NoteDb is not
133 : * allowed.
134 : */
135 104 : INDEX_ONLY,
136 : /**
137 : * This instance was loaded from the change index. Backfilling missing data from NoteDb is
138 : * allowed.
139 : */
140 104 : INDEX_PRIMARY_NOTEDB_SECONDARY,
141 : /** This instance was loaded from NoteDb. */
142 104 : NOTEDB_ONLY
143 : }
144 :
145 : public static List<Change> asChanges(List<ChangeData> changeDatas) {
146 14 : List<Change> result = new ArrayList<>(changeDatas.size());
147 14 : for (ChangeData cd : changeDatas) {
148 12 : result.add(cd.change());
149 12 : }
150 14 : return result;
151 : }
152 :
153 : public static Map<Change.Id, ChangeData> asMap(List<ChangeData> changes) {
154 0 : return changes.stream().collect(toMap(ChangeData::getId, Function.identity()));
155 : }
156 :
157 : public static void ensureChangeLoaded(Iterable<ChangeData> changes) {
158 3 : ChangeData first = Iterables.getFirst(changes, null);
159 3 : if (first == null) {
160 0 : return;
161 : }
162 :
163 3 : for (ChangeData cd : changes) {
164 3 : cd.change();
165 3 : }
166 3 : }
167 :
168 : public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes) {
169 3 : ChangeData first = Iterables.getFirst(changes, null);
170 3 : if (first == null) {
171 0 : return;
172 : }
173 :
174 3 : for (ChangeData cd : changes) {
175 3 : cd.patchSets();
176 3 : }
177 3 : }
178 :
179 : public static void ensureCurrentPatchSetLoaded(Iterable<ChangeData> changes) {
180 1 : ChangeData first = Iterables.getFirst(changes, null);
181 1 : if (first == null) {
182 0 : return;
183 : }
184 :
185 1 : for (ChangeData cd : changes) {
186 1 : cd.currentPatchSet();
187 1 : }
188 1 : }
189 :
190 : public static void ensureCurrentApprovalsLoaded(Iterable<ChangeData> changes) {
191 3 : ChangeData first = Iterables.getFirst(changes, null);
192 3 : if (first == null) {
193 0 : return;
194 : }
195 :
196 3 : for (ChangeData cd : changes) {
197 3 : cd.currentApprovals();
198 3 : }
199 3 : }
200 :
201 : public static void ensureMessagesLoaded(Iterable<ChangeData> changes) {
202 0 : ChangeData first = Iterables.getFirst(changes, null);
203 0 : if (first == null) {
204 0 : return;
205 : }
206 :
207 0 : for (ChangeData cd : changes) {
208 0 : cd.messages();
209 0 : }
210 0 : }
211 :
212 : public static void ensureReviewedByLoadedForOpenChanges(Iterable<ChangeData> changes) {
213 0 : List<ChangeData> pending = new ArrayList<>();
214 0 : for (ChangeData cd : changes) {
215 0 : if (cd.reviewedBy == null && cd.change().isNew()) {
216 0 : pending.add(cd);
217 : }
218 0 : }
219 :
220 0 : if (!pending.isEmpty()) {
221 0 : ensureAllPatchSetsLoaded(pending);
222 0 : ensureMessagesLoaded(pending);
223 0 : for (ChangeData cd : pending) {
224 0 : cd.reviewedBy();
225 0 : }
226 : }
227 0 : }
228 :
229 : public static class Factory {
230 : private final AssistedFactory assistedFactory;
231 :
232 : @Inject
233 151 : Factory(AssistedFactory assistedFactory) {
234 151 : this.assistedFactory = assistedFactory;
235 151 : }
236 :
237 : public ChangeData create(Project.NameKey project, Change.Id id) {
238 103 : return assistedFactory.create(project, id, null, null);
239 : }
240 :
241 : public ChangeData create(Change change) {
242 76 : return assistedFactory.create(change.getProject(), change.getId(), change, null);
243 : }
244 :
245 : public ChangeData create(ChangeNotes notes) {
246 103 : return assistedFactory.create(
247 103 : notes.getChange().getProject(), notes.getChangeId(), notes.getChange(), notes);
248 : }
249 : }
250 :
251 : public interface AssistedFactory {
252 : ChangeData create(
253 : Project.NameKey project,
254 : Change.Id id,
255 : @Nullable Change change,
256 : @Nullable ChangeNotes notes);
257 : }
258 :
259 : /**
260 : * Create an instance for testing only.
261 : *
262 : * <p>Attempting to lazy load data will fail with NPEs. Callers may consider manually setting
263 : * fields that can be set.
264 : *
265 : * @param id change ID
266 : * @return instance for testing.
267 : */
268 : public static ChangeData createForTest(
269 : Project.NameKey project, Change.Id id, int currentPatchSetId, ObjectId commitId) {
270 3 : ChangeData cd =
271 : new ChangeData(
272 : null, null, null, null, null, null, null, null, null, null, null, null, null, null,
273 : null, null, null, project, id, null, null);
274 3 : cd.currentPatchSet =
275 3 : PatchSet.builder()
276 3 : .id(PatchSet.id(id, currentPatchSetId))
277 3 : .commitId(commitId)
278 3 : .uploader(Account.id(1000))
279 3 : .createdOn(TimeUtil.now())
280 3 : .build();
281 3 : return cd;
282 : }
283 :
284 : // Injected fields.
285 : private @Nullable final StarredChangesUtil starredChangesUtil;
286 : private final AllUsersName allUsersName;
287 : private final ApprovalsUtil approvalsUtil;
288 : private final ChangeMessagesUtil cmUtil;
289 : private final ChangeNotes.Factory notesFactory;
290 : private final CommentsUtil commentsUtil;
291 : private final GitRepositoryManager repoManager;
292 : private final MergeUtilFactory mergeUtilFactory;
293 : private final MergeabilityCache mergeabilityCache;
294 : private final PatchListCache patchListCache;
295 : private final PatchSetUtil psUtil;
296 : private final ProjectCache projectCache;
297 : private final TrackingFooters trackingFooters;
298 : private final PureRevert pureRevert;
299 : private final SubmitRequirementsEvaluator submitRequirementsEvaluator;
300 : private final SubmitRequirementsUtil submitRequirementsUtil;
301 : private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
302 :
303 : // Required assisted injected fields.
304 : private final Project.NameKey project;
305 : private final Change.Id legacyId;
306 :
307 : // Lazily populated fields, including optional assisted injected fields.
308 :
309 104 : private final Map<SubmitRuleOptions, List<SubmitRecord>> submitRecords =
310 104 : Maps.newLinkedHashMapWithExpectedSize(1);
311 :
312 : private Map<SubmitRequirement, SubmitRequirementResult> submitRequirements;
313 :
314 104 : private StorageConstraint storageConstraint = StorageConstraint.NOTEDB_ONLY;
315 : private Change change;
316 : private ChangeNotes notes;
317 : private String commitMessage;
318 : private List<FooterLine> commitFooters;
319 : private PatchSet currentPatchSet;
320 : private Collection<PatchSet> patchSets;
321 : private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
322 : private List<PatchSetApproval> currentApprovals;
323 : private List<String> currentFiles;
324 : private Optional<DiffSummary> diffSummary;
325 : private Collection<HumanComment> publishedComments;
326 : private Collection<RobotComment> robotComments;
327 : private CurrentUser visibleTo;
328 : private List<ChangeMessage> messages;
329 : private Optional<ChangedLines> changedLines;
330 : private SubmitTypeRecord submitTypeRecord;
331 : private Boolean mergeable;
332 : private Set<String> hashtags;
333 : /**
334 : * Map from {@link com.google.gerrit.entities.Account.Id} to the tip of the edit ref for this
335 : * change and a given user.
336 : */
337 : private Table<Account.Id, PatchSet.Id, ObjectId> editsByUser;
338 :
339 : private Set<Account.Id> reviewedBy;
340 : /**
341 : * Map from {@link com.google.gerrit.entities.Account.Id} to the tip of the draft comments ref for
342 : * this change and the user.
343 : */
344 : private Map<Account.Id, ObjectId> draftsByUser;
345 :
346 : private ImmutableListMultimap<Account.Id, String> stars;
347 : private StarsOf starsOf;
348 : private ImmutableMap<Account.Id, StarRef> starRefs;
349 : private ReviewerSet reviewers;
350 : private ReviewerByEmailSet reviewersByEmail;
351 : private ReviewerSet pendingReviewers;
352 : private ReviewerByEmailSet pendingReviewersByEmail;
353 : private List<ReviewerStatusUpdate> reviewerUpdates;
354 : private PersonIdent author;
355 : private PersonIdent committer;
356 : private ImmutableSet<AttentionSetUpdate> attentionSet;
357 : private Integer parentCount;
358 : private Integer unresolvedCommentCount;
359 : private Integer totalCommentCount;
360 : private LabelTypes labelTypes;
361 : private Optional<Instant> mergedOn;
362 : private ImmutableSetMultimap<NameKey, RefState> refStates;
363 : private ImmutableList<byte[]> refStatePatterns;
364 :
365 : @Inject
366 : private ChangeData(
367 : @Nullable StarredChangesUtil starredChangesUtil,
368 : ApprovalsUtil approvalsUtil,
369 : AllUsersName allUsersName,
370 : ChangeMessagesUtil cmUtil,
371 : ChangeNotes.Factory notesFactory,
372 : CommentsUtil commentsUtil,
373 : GitRepositoryManager repoManager,
374 : MergeUtilFactory mergeUtilFactory,
375 : MergeabilityCache mergeabilityCache,
376 : PatchListCache patchListCache,
377 : PatchSetUtil psUtil,
378 : ProjectCache projectCache,
379 : TrackingFooters trackingFooters,
380 : PureRevert pureRevert,
381 : SubmitRequirementsEvaluator submitRequirementsEvaluator,
382 : SubmitRequirementsUtil submitRequirementsUtil,
383 : SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
384 : @Assisted Project.NameKey project,
385 : @Assisted Change.Id id,
386 : @Assisted @Nullable Change change,
387 104 : @Assisted @Nullable ChangeNotes notes) {
388 104 : this.approvalsUtil = approvalsUtil;
389 104 : this.allUsersName = allUsersName;
390 104 : this.cmUtil = cmUtil;
391 104 : this.notesFactory = notesFactory;
392 104 : this.commentsUtil = commentsUtil;
393 104 : this.repoManager = repoManager;
394 104 : this.mergeUtilFactory = mergeUtilFactory;
395 104 : this.mergeabilityCache = mergeabilityCache;
396 104 : this.patchListCache = patchListCache;
397 104 : this.psUtil = psUtil;
398 104 : this.projectCache = projectCache;
399 104 : this.starredChangesUtil = starredChangesUtil;
400 104 : this.trackingFooters = trackingFooters;
401 104 : this.pureRevert = pureRevert;
402 104 : this.submitRequirementsEvaluator = submitRequirementsEvaluator;
403 104 : this.submitRequirementsUtil = submitRequirementsUtil;
404 104 : this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
405 :
406 104 : this.project = project;
407 104 : this.legacyId = id;
408 :
409 104 : this.change = change;
410 104 : this.notes = notes;
411 104 : }
412 :
413 : /**
414 : * If false, omit fields that require database/repo IO.
415 : *
416 : * <p>This is used to enforce that the dashboard is rendered from the index only. If {@code
417 : * lazyLoad} is on, the {@code ChangeData} object will load from the database ("lazily") when a
418 : * field accessor is called.
419 : */
420 : public ChangeData setStorageConstraint(StorageConstraint storageConstraint) {
421 33 : this.storageConstraint = storageConstraint;
422 33 : return this;
423 : }
424 :
425 : public StorageConstraint getStorageConstraint() {
426 103 : return storageConstraint;
427 : }
428 :
429 : /** Returns {@code true} if we allow reading data from NoteDb. */
430 : public boolean lazyload() {
431 103 : return storageConstraint.ordinal()
432 103 : >= StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY.ordinal();
433 : }
434 :
435 : public AllUsersName getAllUsersNameForIndexing() {
436 103 : return allUsersName;
437 : }
438 :
439 : @VisibleForTesting
440 : public void setCurrentFilePaths(List<String> filePaths) {
441 1 : PatchSet ps = currentPatchSet();
442 1 : if (ps != null) {
443 1 : currentFiles = ImmutableList.copyOf(filePaths);
444 : }
445 1 : }
446 :
447 : public List<String> currentFilePaths() {
448 104 : if (currentFiles == null) {
449 103 : if (!lazyload()) {
450 0 : return Collections.emptyList();
451 : }
452 103 : Optional<DiffSummary> p = getDiffSummary();
453 103 : currentFiles = p.map(DiffSummary::getPaths).orElse(Collections.emptyList());
454 : }
455 104 : return currentFiles;
456 : }
457 :
458 : private Optional<DiffSummary> getDiffSummary() {
459 103 : if (diffSummary == null) {
460 103 : if (!lazyload()) {
461 0 : return Optional.empty();
462 : }
463 :
464 103 : Change c = change();
465 103 : PatchSet ps = currentPatchSet();
466 103 : if (c == null || ps == null || !loadCommitData()) {
467 0 : return Optional.empty();
468 : }
469 :
470 103 : PatchListKey pk = PatchListKey.againstBase(ps.commitId(), parentCount);
471 103 : DiffSummaryKey key = DiffSummaryKey.fromPatchListKey(pk);
472 : try {
473 103 : diffSummary = Optional.of(patchListCache.getDiffSummary(key, c.getProject()));
474 0 : } catch (PatchListNotAvailableException e) {
475 0 : diffSummary = Optional.empty();
476 103 : }
477 : }
478 103 : return diffSummary;
479 : }
480 :
481 : private Optional<ChangedLines> computeChangedLines() {
482 103 : Optional<DiffSummary> ds = getDiffSummary();
483 103 : if (ds.isPresent()) {
484 103 : return Optional.of(ds.get().getChangedLines());
485 : }
486 0 : return Optional.empty();
487 : }
488 :
489 : public Optional<ChangedLines> changedLines() {
490 103 : if (changedLines == null) {
491 103 : if (!lazyload()) {
492 1 : return Optional.empty();
493 : }
494 103 : changedLines = computeChangedLines();
495 : }
496 103 : return changedLines;
497 : }
498 :
499 : public void setChangedLines(int insertions, int deletions) {
500 0 : changedLines = Optional.of(new ChangedLines(insertions, deletions));
501 0 : }
502 :
503 : public void setLinesInserted(int insertions) {
504 100 : changedLines =
505 100 : Optional.of(
506 : new ChangedLines(
507 : insertions,
508 100 : changedLines != null && changedLines.isPresent()
509 0 : ? changedLines.get().deletions
510 100 : : -1));
511 100 : }
512 :
513 : public void setLinesDeleted(int deletions) {
514 100 : changedLines =
515 100 : Optional.of(
516 : new ChangedLines(
517 100 : changedLines != null && changedLines.isPresent()
518 100 : ? changedLines.get().insertions
519 100 : : -1,
520 : deletions));
521 100 : }
522 :
523 : public void setNoChangedLines() {
524 0 : changedLines = Optional.empty();
525 0 : }
526 :
527 : public Change.Id getId() {
528 104 : return legacyId;
529 : }
530 :
531 : public Project.NameKey project() {
532 103 : return project;
533 : }
534 :
535 : boolean fastIsVisibleTo(CurrentUser user) {
536 76 : return visibleTo == user;
537 : }
538 :
539 : void cacheVisibleTo(CurrentUser user) {
540 76 : visibleTo = user;
541 76 : }
542 :
543 : public Change change() {
544 104 : if (change == null && lazyload()) {
545 103 : reloadChange();
546 : }
547 104 : return change;
548 : }
549 :
550 : public void setChange(Change c) {
551 101 : change = c;
552 101 : }
553 :
554 : public Change reloadChange() {
555 : try {
556 103 : notes = notesFactory.createChecked(project, legacyId);
557 8 : } catch (NoSuchChangeException e) {
558 8 : throw new StorageException("Unable to load change " + legacyId, e);
559 103 : }
560 103 : change = notes.getChange();
561 103 : setPatchSets(null);
562 103 : return change;
563 : }
564 :
565 : public LabelTypes getLabelTypes() {
566 103 : if (labelTypes == null) {
567 103 : ProjectState state = projectCache.get(project()).orElseThrow(illegalState(project()));
568 103 : labelTypes = state.getLabelTypes(change().getDest());
569 : }
570 103 : return labelTypes;
571 : }
572 :
573 : public ChangeNotes notes() {
574 103 : if (notes == null) {
575 98 : if (!lazyload()) {
576 0 : throw new StorageException("ChangeNotes not available, lazyLoad = false");
577 : }
578 98 : notes = notesFactory.create(project(), legacyId);
579 : }
580 103 : return notes;
581 : }
582 :
583 : @Nullable
584 : public PatchSet currentPatchSet() {
585 104 : if (currentPatchSet == null) {
586 104 : Change c = change();
587 104 : if (c == null) {
588 0 : return null;
589 : }
590 104 : for (PatchSet p : patchSets()) {
591 104 : if (p.id().equals(c.currentPatchSetId())) {
592 103 : currentPatchSet = p;
593 103 : return p;
594 : }
595 55 : }
596 : }
597 104 : return currentPatchSet;
598 : }
599 :
600 : public List<PatchSetApproval> currentApprovals() {
601 103 : if (currentApprovals == null) {
602 103 : if (!lazyload()) {
603 0 : return Collections.emptyList();
604 : }
605 103 : Change c = change();
606 103 : if (c == null) {
607 0 : currentApprovals = Collections.emptyList();
608 : } else {
609 : try {
610 103 : currentApprovals =
611 103 : ImmutableList.copyOf(approvalsUtil.byPatchSet(notes(), c.currentPatchSetId()));
612 0 : } catch (StorageException e) {
613 0 : if (e.getCause() instanceof NoSuchChangeException) {
614 0 : currentApprovals = Collections.emptyList();
615 : } else {
616 0 : throw e;
617 : }
618 103 : }
619 : }
620 : }
621 103 : return currentApprovals;
622 : }
623 :
624 : public void setCurrentApprovals(List<PatchSetApproval> approvals) {
625 100 : currentApprovals = approvals;
626 100 : }
627 :
628 : @Nullable
629 : public String commitMessage() {
630 103 : if (commitMessage == null) {
631 15 : if (!loadCommitData()) {
632 0 : return null;
633 : }
634 : }
635 103 : return commitMessage;
636 : }
637 :
638 : /** Returns the list of commit footers (which may be empty). */
639 : public List<FooterLine> commitFooters() {
640 103 : if (commitFooters == null) {
641 16 : if (!loadCommitData()) {
642 0 : return ImmutableList.of();
643 : }
644 : }
645 103 : return commitFooters;
646 : }
647 :
648 : public ListMultimap<String, String> trackingFooters() {
649 103 : return trackingFooters.extract(commitFooters());
650 : }
651 :
652 : @Nullable
653 : public PersonIdent getAuthor() {
654 103 : if (author == null) {
655 3 : if (!loadCommitData()) {
656 0 : return null;
657 : }
658 : }
659 103 : return author;
660 : }
661 :
662 : @Nullable
663 : public PersonIdent getCommitter() {
664 103 : if (committer == null) {
665 2 : if (!loadCommitData()) {
666 0 : return null;
667 : }
668 : }
669 103 : return committer;
670 : }
671 :
672 : private boolean loadCommitData() {
673 103 : PatchSet ps = currentPatchSet();
674 103 : if (ps == null) {
675 0 : return false;
676 : }
677 103 : try (Repository repo = repoManager.openRepository(project());
678 103 : RevWalk walk = new RevWalk(repo)) {
679 103 : RevCommit c = walk.parseCommit(ps.commitId());
680 103 : commitMessage = c.getFullMessage();
681 103 : commitFooters = c.getFooterLines();
682 103 : author = c.getAuthorIdent();
683 103 : committer = c.getCommitterIdent();
684 103 : parentCount = c.getParentCount();
685 0 : } catch (IOException e) {
686 0 : throw new StorageException(
687 0 : String.format(
688 : "Loading commit %s for ps %d of change %d failed.",
689 0 : ps.commitId(), ps.id().get(), ps.id().changeId().get()),
690 : e);
691 103 : }
692 103 : return true;
693 : }
694 :
695 : /** Returns the most recent update (i.e. status) per user. */
696 : public ImmutableSet<AttentionSetUpdate> attentionSet() {
697 103 : if (attentionSet == null) {
698 103 : if (!lazyload()) {
699 1 : return ImmutableSet.of();
700 : }
701 103 : attentionSet = notes().getAttentionSet();
702 : }
703 103 : return attentionSet;
704 : }
705 :
706 : /**
707 : * Returns the {@link Optional} value of time when the change was merged.
708 : *
709 : * <p>The value can be set from index field, see {@link ChangeData#setMergedOn} or loaded from the
710 : * database (available in {@link ChangeNotes})
711 : *
712 : * @return {@link Optional} value of time when the change was merged.
713 : * @throws StorageException if {@code lazyLoad} is off, {@link ChangeNotes} can not be loaded
714 : * because we do not expect to call the database.
715 : */
716 : public Optional<Instant> getMergedOn() throws StorageException {
717 103 : if (mergedOn == null) {
718 : // The value was not loaded yet, try to get from the database.
719 103 : mergedOn = notes().getMergedOn();
720 : }
721 103 : return mergedOn;
722 : }
723 :
724 : /** Sets the value e.g. when loading from index. */
725 : public void setMergedOn(@Nullable Instant mergedOn) {
726 100 : this.mergedOn = Optional.ofNullable(mergedOn);
727 100 : }
728 :
729 : /**
730 : * Sets the specified attention set. If two or more entries refer to the same user, throws an
731 : * {@link IllegalStateException}.
732 : */
733 : public void setAttentionSet(ImmutableSet<AttentionSetUpdate> attentionSet) {
734 100 : if (attentionSet.stream().map(AttentionSetUpdate::account).distinct().count()
735 100 : != attentionSet.size()) {
736 0 : throw new IllegalStateException(
737 0 : String.format(
738 : "Stored attention set for change %d contains duplicate update",
739 0 : change.getId().get()));
740 : }
741 100 : this.attentionSet = attentionSet;
742 100 : }
743 :
744 : /** Returns patches for the change, in patch set ID order. */
745 : public Collection<PatchSet> patchSets() {
746 104 : if (patchSets == null) {
747 103 : patchSets = psUtil.byChange(notes());
748 : }
749 104 : return patchSets;
750 : }
751 :
752 : public void setPatchSets(Collection<PatchSet> patchSets) {
753 104 : this.currentPatchSet = null;
754 104 : this.patchSets = patchSets;
755 104 : }
756 :
757 : /** Returns patch with the given ID, or null if it does not exist. */
758 : @Nullable
759 : public PatchSet patchSet(PatchSet.Id psId) {
760 13 : if (currentPatchSet != null && currentPatchSet.id().equals(psId)) {
761 6 : return currentPatchSet;
762 : }
763 12 : for (PatchSet ps : patchSets()) {
764 12 : if (ps.id().equals(psId)) {
765 12 : return ps;
766 : }
767 5 : }
768 1 : return null;
769 : }
770 :
771 : /**
772 : * Returns all patch set approvals for the change, keyed by ID, ordered by timestamp within each
773 : * patch set.
774 : */
775 : public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() {
776 103 : if (allApprovals == null) {
777 103 : if (!lazyload()) {
778 8 : return ImmutableListMultimap.of();
779 : }
780 103 : allApprovals = approvalsUtil.byChangeExcludingCopiedApprovals(notes());
781 : }
782 103 : return allApprovals;
783 : }
784 :
785 : /* @return legacy submit ('SUBM') approval label */
786 : // TODO(mariasavtchouk): Deprecate legacy submit label,
787 : // see com.google.gerrit.entities.LabelId.LEGACY_SUBMIT_NAME
788 : public Optional<PatchSetApproval> getSubmitApproval() {
789 103 : return currentApprovals().stream().filter(PatchSetApproval::isLegacySubmit).findFirst();
790 : }
791 :
792 : public ReviewerSet reviewers() {
793 103 : if (reviewers == null) {
794 103 : if (!lazyload()) {
795 : // We are not allowed to load values from NoteDb. Reviewers were not populated with values
796 : // from the index. However, we need these values for permission checks.
797 0 : throw new IllegalStateException("reviewers not populated");
798 : }
799 103 : reviewers = approvalsUtil.getReviewers(notes());
800 : }
801 103 : return reviewers;
802 : }
803 :
804 : public void setReviewers(ReviewerSet reviewers) {
805 100 : this.reviewers = reviewers;
806 100 : }
807 :
808 : public ReviewerByEmailSet reviewersByEmail() {
809 103 : if (reviewersByEmail == null) {
810 103 : if (!lazyload()) {
811 0 : return ReviewerByEmailSet.empty();
812 : }
813 103 : reviewersByEmail = notes().getReviewersByEmail();
814 : }
815 103 : return reviewersByEmail;
816 : }
817 :
818 : public void setReviewersByEmail(ReviewerByEmailSet reviewersByEmail) {
819 100 : this.reviewersByEmail = reviewersByEmail;
820 100 : }
821 :
822 : public ReviewerByEmailSet getReviewersByEmail() {
823 0 : return reviewersByEmail;
824 : }
825 :
826 : public void setPendingReviewers(ReviewerSet pendingReviewers) {
827 100 : this.pendingReviewers = pendingReviewers;
828 100 : }
829 :
830 : public ReviewerSet getPendingReviewers() {
831 0 : return this.pendingReviewers;
832 : }
833 :
834 : public ReviewerSet pendingReviewers() {
835 103 : if (pendingReviewers == null) {
836 103 : if (!lazyload()) {
837 0 : return ReviewerSet.empty();
838 : }
839 103 : pendingReviewers = notes().getPendingReviewers();
840 : }
841 103 : return pendingReviewers;
842 : }
843 :
844 : public void setPendingReviewersByEmail(ReviewerByEmailSet pendingReviewersByEmail) {
845 100 : this.pendingReviewersByEmail = pendingReviewersByEmail;
846 100 : }
847 :
848 : public ReviewerByEmailSet getPendingReviewersByEmail() {
849 0 : return pendingReviewersByEmail;
850 : }
851 :
852 : public ReviewerByEmailSet pendingReviewersByEmail() {
853 103 : if (pendingReviewersByEmail == null) {
854 103 : if (!lazyload()) {
855 0 : return ReviewerByEmailSet.empty();
856 : }
857 103 : pendingReviewersByEmail = notes().getPendingReviewersByEmail();
858 : }
859 103 : return pendingReviewersByEmail;
860 : }
861 :
862 : public List<ReviewerStatusUpdate> reviewerUpdates() {
863 103 : if (reviewerUpdates == null) {
864 103 : if (!lazyload()) {
865 1 : return Collections.emptyList();
866 : }
867 103 : reviewerUpdates = approvalsUtil.getReviewerUpdates(notes());
868 : }
869 103 : return reviewerUpdates;
870 : }
871 :
872 : public void setReviewerUpdates(List<ReviewerStatusUpdate> reviewerUpdates) {
873 0 : this.reviewerUpdates = reviewerUpdates;
874 0 : }
875 :
876 : public List<ReviewerStatusUpdate> getReviewerUpdates() {
877 0 : return reviewerUpdates;
878 : }
879 :
880 : public Collection<HumanComment> publishedComments() {
881 103 : if (publishedComments == null) {
882 103 : if (!lazyload()) {
883 0 : return Collections.emptyList();
884 : }
885 103 : publishedComments = commentsUtil.publishedHumanCommentsByChange(notes());
886 : }
887 103 : return publishedComments;
888 : }
889 :
890 : public Collection<RobotComment> robotComments() {
891 103 : if (robotComments == null) {
892 103 : if (!lazyload()) {
893 0 : return Collections.emptyList();
894 : }
895 103 : robotComments = commentsUtil.robotCommentsByChange(notes());
896 : }
897 103 : return robotComments;
898 : }
899 :
900 : @Nullable
901 : public Integer unresolvedCommentCount() {
902 103 : if (unresolvedCommentCount == null) {
903 103 : if (!lazyload()) {
904 1 : return null;
905 : }
906 :
907 103 : List<Comment> comments =
908 103 : Stream.concat(publishedComments().stream(), robotComments().stream()).collect(toList());
909 :
910 103 : ImmutableSet<CommentThread<Comment>> commentThreads =
911 103 : CommentThreads.forComments(comments).getThreads();
912 103 : unresolvedCommentCount =
913 103 : (int) commentThreads.stream().filter(CommentThread::unresolved).count();
914 : }
915 :
916 103 : return unresolvedCommentCount;
917 : }
918 :
919 : public void setUnresolvedCommentCount(Integer count) {
920 100 : this.unresolvedCommentCount = count;
921 100 : }
922 :
923 : @Nullable
924 : public Integer totalCommentCount() {
925 103 : if (totalCommentCount == null) {
926 103 : if (!lazyload()) {
927 1 : return null;
928 : }
929 :
930 : // Fail on overflow.
931 103 : totalCommentCount =
932 103 : Ints.checkedCast((long) publishedComments().size() + robotComments().size());
933 : }
934 103 : return totalCommentCount;
935 : }
936 :
937 : public void setTotalCommentCount(Integer count) {
938 100 : this.totalCommentCount = count;
939 100 : }
940 :
941 : public List<ChangeMessage> messages() {
942 103 : if (messages == null) {
943 103 : if (!lazyload()) {
944 0 : return Collections.emptyList();
945 : }
946 103 : messages = cmUtil.byChange(notes());
947 : }
948 103 : return messages;
949 : }
950 :
951 : /**
952 : * Similar to {@link #submitRequirements()}, except that it also converts submit records resulting
953 : * from the evaluation of legacy submit rules to submit requirements.
954 : */
955 : public Map<SubmitRequirement, SubmitRequirementResult> submitRequirementsIncludingLegacy() {
956 103 : Map<SubmitRequirement, SubmitRequirementResult> projectConfigReqs = submitRequirements();
957 103 : Map<SubmitRequirement, SubmitRequirementResult> legacyReqs =
958 103 : SubmitRequirementsAdapter.getLegacyRequirements(this);
959 103 : return submitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
960 : projectConfigReqs, legacyReqs, this);
961 : }
962 :
963 : /**
964 : * Get all evaluated submit requirements for this change, including those from parent projects.
965 : * For closed changes, submit requirements are read from the change notes. For active changes,
966 : * submit requirements are evaluated online.
967 : *
968 : * <p>For changes loaded from the index, the value will be set from index field {@link
969 : * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS}.
970 : */
971 : public Map<SubmitRequirement, SubmitRequirementResult> submitRequirements() {
972 103 : if (submitRequirements == null) {
973 103 : if (!lazyload()) {
974 1 : return Collections.emptyMap();
975 : }
976 103 : Change c = change();
977 103 : if (c == null || !c.isClosed()) {
978 : // Open changes: Evaluate submit requirements online.
979 103 : submitRequirements = submitRequirementsEvaluator.evaluateAllRequirements(this);
980 103 : return submitRequirements;
981 : }
982 : // Closed changes: Load submit requirement results from NoteDb.
983 57 : submitRequirements =
984 57 : notes().getSubmitRequirementsResult().stream()
985 57 : .filter(r -> !r.isLegacy())
986 57 : .collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
987 : }
988 103 : return submitRequirements;
989 : }
990 :
991 : public void setSubmitRequirements(
992 : Map<SubmitRequirement, SubmitRequirementResult> submitRequirements) {
993 100 : this.submitRequirements = submitRequirements;
994 100 : }
995 :
996 : public List<SubmitRecord> submitRecords(SubmitRuleOptions options) {
997 : // If the change is not submitted yet, 'strict' and 'lenient' both have the same result. If the
998 : // change is submitted, SubmitRecord requested with 'strict' will contain just a single entry
999 : // that with status=CLOSED. The latter is cheap to evaluate as we don't have to run any actual
1000 : // evaluation.
1001 103 : List<SubmitRecord> records = submitRecords.get(options);
1002 103 : if (records == null) {
1003 103 : if (storageConstraint != StorageConstraint.NOTEDB_ONLY) {
1004 : // Submit requirements are expensive. We allow loading them only if this change did not
1005 : // originate from the change index and we can invest the extra time.
1006 0 : logger.atWarning().log(
1007 : "Tried to load SubmitRecords for change fetched from index %s: %d",
1008 0 : project(), getId().get());
1009 0 : return Collections.emptyList();
1010 : }
1011 103 : records = submitRuleEvaluatorFactory.create(options).evaluate(this);
1012 103 : submitRecords.put(options, records);
1013 103 : if (!change().isClosed() && submitRecords.size() == 1) {
1014 : // Cache the SubmitRecord with allowClosed = !allowClosed as the SubmitRecord are the same.
1015 103 : submitRecords.put(
1016 : options
1017 103 : .toBuilder()
1018 103 : .recomputeOnClosedChanges(!options.recomputeOnClosedChanges())
1019 103 : .build(),
1020 : records);
1021 : }
1022 : }
1023 103 : return records;
1024 : }
1025 :
1026 : public void setSubmitRecords(SubmitRuleOptions options, List<SubmitRecord> records) {
1027 100 : submitRecords.put(options, records);
1028 100 : }
1029 :
1030 : public SubmitTypeRecord submitTypeRecord() {
1031 103 : if (submitTypeRecord == null) {
1032 103 : submitTypeRecord =
1033 103 : submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults()).getSubmitType(this);
1034 : }
1035 103 : return submitTypeRecord;
1036 : }
1037 :
1038 : public void setMergeable(Boolean mergeable) {
1039 100 : this.mergeable = mergeable;
1040 100 : }
1041 :
1042 : @Nullable
1043 : public Boolean isMergeable() {
1044 20 : if (mergeable == null) {
1045 20 : Change c = change();
1046 20 : if (c == null) {
1047 0 : return null;
1048 : }
1049 20 : if (c.isMerged()) {
1050 12 : mergeable = true;
1051 20 : } else if (c.isAbandoned()) {
1052 1 : return null;
1053 20 : } else if (c.isWorkInProgress()) {
1054 0 : return null;
1055 : } else {
1056 20 : if (!lazyload()) {
1057 0 : return null;
1058 : }
1059 20 : PatchSet ps = currentPatchSet();
1060 20 : if (ps == null) {
1061 0 : return null;
1062 : }
1063 :
1064 20 : try (Repository repo = repoManager.openRepository(project())) {
1065 20 : Ref ref = repo.getRefDatabase().exactRef(c.getDest().branch());
1066 20 : SubmitTypeRecord str = submitTypeRecord();
1067 20 : if (!str.isOk()) {
1068 : // If submit type rules are broken, it's definitely not mergeable.
1069 : // No need to log, as SubmitRuleEvaluator already did it for us.
1070 0 : return false;
1071 : }
1072 20 : String mergeStrategy =
1073 : mergeUtilFactory
1074 20 : .create(projectCache.get(project()).orElseThrow(illegalState(project())))
1075 20 : .mergeStrategyName();
1076 20 : mergeable =
1077 20 : mergeabilityCache.get(ps.commitId(), ref, str.type, mergeStrategy, c.getDest(), repo);
1078 0 : } catch (IOException e) {
1079 0 : throw new StorageException(e);
1080 20 : }
1081 : }
1082 : }
1083 20 : return mergeable;
1084 : }
1085 :
1086 : @Nullable
1087 : public Boolean isMerge() {
1088 103 : if (parentCount == null) {
1089 2 : if (!loadCommitData()) {
1090 0 : return null;
1091 : }
1092 : }
1093 103 : return parentCount > 1;
1094 : }
1095 :
1096 : public Set<Account.Id> editsByUser() {
1097 103 : return editRefs().rowKeySet();
1098 : }
1099 :
1100 : public Table<Account.Id, PatchSet.Id, ObjectId> editRefs() {
1101 103 : if (editsByUser == null) {
1102 103 : if (!lazyload()) {
1103 0 : return HashBasedTable.create();
1104 : }
1105 103 : Change c = change();
1106 103 : if (c == null) {
1107 0 : return HashBasedTable.create();
1108 : }
1109 103 : editsByUser = HashBasedTable.create();
1110 103 : Change.Id id = requireNonNull(change.getId());
1111 103 : try (Repository repo = repoManager.openRepository(project())) {
1112 103 : for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_USERS)) {
1113 29 : if (!RefNames.isRefsEdit(ref.getName())) {
1114 3 : continue;
1115 : }
1116 27 : PatchSet.Id ps = PatchSet.Id.fromEditRef(ref.getName());
1117 27 : if (id.equals(ps.changeId())) {
1118 27 : Account.Id accountId = Account.Id.fromRef(ref.getName());
1119 27 : if (accountId != null) {
1120 27 : editsByUser.put(accountId, ps, ref.getObjectId());
1121 : }
1122 : }
1123 27 : }
1124 0 : } catch (IOException e) {
1125 0 : throw new StorageException(e);
1126 103 : }
1127 : }
1128 103 : return editsByUser;
1129 : }
1130 :
1131 : public Set<Account.Id> draftsByUser() {
1132 3 : return draftRefs().keySet();
1133 : }
1134 :
1135 : public boolean isReviewedBy(Account.Id accountId) {
1136 103 : return reviewedBy().contains(accountId);
1137 : }
1138 :
1139 : public Set<Account.Id> reviewedBy() {
1140 103 : if (reviewedBy == null) {
1141 103 : if (!lazyload()) {
1142 0 : return Collections.emptySet();
1143 : }
1144 103 : Change c = change();
1145 103 : if (c == null) {
1146 0 : return Collections.emptySet();
1147 : }
1148 103 : List<ReviewedByEvent> events = new ArrayList<>();
1149 103 : for (ChangeMessage msg : messages()) {
1150 103 : if (msg.getAuthor() != null) {
1151 103 : events.add(ReviewedByEvent.create(msg));
1152 : }
1153 103 : }
1154 103 : events = Lists.reverse(events);
1155 103 : reviewedBy = new LinkedHashSet<>();
1156 103 : Account.Id owner = c.getOwner();
1157 103 : for (ReviewedByEvent event : events) {
1158 103 : if (owner.equals(event.author())) {
1159 103 : break;
1160 : }
1161 41 : reviewedBy.add(event.author());
1162 41 : }
1163 : }
1164 103 : return reviewedBy;
1165 : }
1166 :
1167 : public void setReviewedBy(Set<Account.Id> reviewedBy) {
1168 100 : this.reviewedBy = reviewedBy;
1169 100 : }
1170 :
1171 : public Set<String> hashtags() {
1172 103 : if (hashtags == null) {
1173 103 : if (!lazyload()) {
1174 1 : return Collections.emptySet();
1175 : }
1176 103 : hashtags = notes().getHashtags();
1177 : }
1178 103 : return hashtags;
1179 : }
1180 :
1181 : public void setHashtags(Set<String> hashtags) {
1182 100 : this.hashtags = hashtags;
1183 100 : }
1184 :
1185 : public ImmutableListMultimap<Account.Id, String> stars() {
1186 103 : if (stars == null) {
1187 103 : if (!lazyload()) {
1188 0 : return ImmutableListMultimap.of();
1189 : }
1190 103 : ImmutableListMultimap.Builder<Account.Id, String> b = ImmutableListMultimap.builder();
1191 103 : for (Map.Entry<Account.Id, StarRef> e : starRefs().entrySet()) {
1192 1 : b.putAll(e.getKey(), e.getValue().labels());
1193 1 : }
1194 103 : return b.build();
1195 : }
1196 2 : return stars;
1197 : }
1198 :
1199 : public void setStars(ListMultimap<Account.Id, String> stars) {
1200 3 : this.stars = ImmutableListMultimap.copyOf(stars);
1201 3 : }
1202 :
1203 : private ImmutableMap<Account.Id, StarRef> starRefs() {
1204 103 : if (starRefs == null) {
1205 103 : if (!lazyload()) {
1206 0 : return ImmutableMap.of();
1207 : }
1208 103 : starRefs = requireNonNull(starredChangesUtil).byChange(legacyId);
1209 : }
1210 103 : return starRefs;
1211 : }
1212 :
1213 : public Set<String> stars(Account.Id accountId) {
1214 103 : if (starsOf != null) {
1215 31 : if (!starsOf.accountId().equals(accountId)) {
1216 0 : starsOf = null;
1217 : }
1218 : }
1219 103 : if (starsOf == null) {
1220 103 : if (stars != null) {
1221 2 : starsOf = StarsOf.create(accountId, stars.get(accountId));
1222 : } else {
1223 103 : if (!lazyload()) {
1224 30 : return ImmutableSet.of();
1225 : }
1226 103 : starsOf = StarsOf.create(accountId, starredChangesUtil.getLabels(accountId, legacyId));
1227 : }
1228 : }
1229 103 : return starsOf.stars();
1230 : }
1231 :
1232 : /**
1233 : * Returns {@code null} if {@code revertOf} is {@code null}; true if the change is a pure revert;
1234 : * false otherwise.
1235 : */
1236 : @Nullable
1237 : public Boolean isPureRevert() {
1238 103 : if (change().getRevertOf() == null) {
1239 103 : return null;
1240 : }
1241 : try {
1242 14 : return pureRevert.get(notes(), Optional.empty());
1243 0 : } catch (IOException | BadRequestException | ResourceConflictException e) {
1244 0 : throw new StorageException("could not compute pure revert", e);
1245 : }
1246 : }
1247 :
1248 : @Override
1249 : public String toString() {
1250 4 : MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
1251 4 : if (change != null) {
1252 4 : h.addValue(change);
1253 : } else {
1254 0 : h.addValue(legacyId);
1255 : }
1256 4 : return h.toString();
1257 : }
1258 :
1259 : public static class ChangedLines {
1260 : public final int insertions;
1261 : public final int deletions;
1262 :
1263 103 : public ChangedLines(int insertions, int deletions) {
1264 103 : this.insertions = insertions;
1265 103 : this.deletions = deletions;
1266 103 : }
1267 : }
1268 :
1269 : public SetMultimap<NameKey, RefState> getRefStates() {
1270 103 : if (refStates == null) {
1271 103 : if (!lazyload()) {
1272 1 : return ImmutableSetMultimap.of();
1273 : }
1274 :
1275 103 : ImmutableSetMultimap.Builder<NameKey, RefState> result = ImmutableSetMultimap.builder();
1276 103 : for (Table.Cell<Account.Id, PatchSet.Id, ObjectId> edit : editRefs().cellSet()) {
1277 27 : result.put(
1278 : project,
1279 27 : RefState.create(
1280 27 : RefNames.refsEdit(
1281 27 : edit.getRowKey(), edit.getColumnKey().changeId(), edit.getColumnKey()),
1282 27 : edit.getValue()));
1283 27 : }
1284 :
1285 : // TODO: instantiating the notes is too much. We don't want to parse NoteDb, we just want the
1286 : // refs.
1287 103 : result.put(project, RefState.create(notes().getRefName(), notes().getMetaId()));
1288 103 : notes().getRobotComments(); // Force loading robot comments.
1289 103 : RobotCommentNotes robotNotes = notes().getRobotCommentNotes();
1290 103 : result.put(project, RefState.create(robotNotes.getRefName(), robotNotes.getMetaId()));
1291 :
1292 103 : refStates = result.build();
1293 : }
1294 :
1295 103 : return refStates;
1296 : }
1297 :
1298 : public void setRefStates(ImmutableSetMultimap<Project.NameKey, RefState> refStates) {
1299 100 : this.refStates = refStates;
1300 100 : if (draftsByUser == null) {
1301 : // Recover draft refs as well. Draft comments are represented as refs in the repository.
1302 : // ChangeData exposes #draftsByUser which just provides a Set of Account.Ids of users who
1303 : // have drafts comments on this change. Recovering this list from RefStates makes it
1304 : // available even on ChangeData instances retrieved from the index.
1305 100 : draftsByUser = new HashMap<>();
1306 100 : if (refStates.containsKey(allUsersName)) {
1307 3 : refStates.get(allUsersName).stream()
1308 3 : .filter(r -> RefNames.isRefsDraftsComments(r.ref()))
1309 3 : .forEach(r -> draftsByUser.put(Account.Id.fromRef(r.ref()), r.id()));
1310 : }
1311 : }
1312 100 : if (editsByUser == null) {
1313 : // Recover edit refs as well. Edits are represented as refs in the repository.
1314 : // ChangeData exposes #editsByUser which just provides a Set of Account.Ids of users who
1315 : // have edits on this change. Recovering this list from RefStates makes it available even
1316 : // on ChangeData instances retrieved from the index.
1317 100 : editsByUser = HashBasedTable.create();
1318 100 : if (refStates.containsKey(project())) {
1319 100 : refStates.get(project()).stream()
1320 100 : .filter(r -> RefNames.isRefsEdit(r.ref()))
1321 100 : .forEach(
1322 : r ->
1323 26 : editsByUser.put(
1324 26 : Account.Id.fromRef(r.ref()), PatchSet.Id.fromEditRef(r.ref()), r.id()));
1325 : }
1326 : }
1327 100 : }
1328 :
1329 : public ImmutableList<byte[]> getRefStatePatterns() {
1330 4 : return refStatePatterns;
1331 : }
1332 :
1333 : public void setRefStatePatterns(Iterable<byte[]> refStatePatterns) {
1334 100 : this.refStatePatterns = ImmutableList.copyOf(refStatePatterns);
1335 100 : }
1336 :
1337 : @AutoValue
1338 103 : abstract static class ReviewedByEvent {
1339 : private static ReviewedByEvent create(ChangeMessage msg) {
1340 103 : return new AutoValue_ChangeData_ReviewedByEvent(msg.getAuthor(), msg.getWrittenOn());
1341 : }
1342 :
1343 : public abstract Account.Id author();
1344 :
1345 : public abstract Instant ts();
1346 : }
1347 :
1348 : @AutoValue
1349 103 : abstract static class StarsOf {
1350 : private static StarsOf create(Account.Id accountId, Iterable<String> stars) {
1351 103 : return new AutoValue_ChangeData_StarsOf(accountId, ImmutableSortedSet.copyOf(stars));
1352 : }
1353 :
1354 : public abstract Account.Id accountId();
1355 :
1356 : public abstract ImmutableSortedSet<String> stars();
1357 : }
1358 :
1359 : private Map<Account.Id, ObjectId> draftRefs() {
1360 3 : if (draftsByUser == null) {
1361 3 : if (!lazyload()) {
1362 0 : return Collections.emptyMap();
1363 : }
1364 3 : Change c = change();
1365 3 : if (c == null) {
1366 0 : return Collections.emptyMap();
1367 : }
1368 :
1369 3 : draftsByUser = new HashMap<>();
1370 3 : for (Ref ref : commentsUtil.getDraftRefs(notes().getChangeId())) {
1371 0 : Account.Id account = Account.Id.fromRefSuffix(ref.getName());
1372 0 : if (account != null
1373 : // Double-check that any drafts exist for this user after
1374 : // filtering out zombies. If some but not all drafts in the ref
1375 : // were zombies, the returned Ref still includes those zombies;
1376 : // this is suboptimal, but is ok for the purposes of
1377 : // draftsByUser(), and easier than trying to rebuild the change at
1378 : // this point.
1379 0 : && !notes().getDraftComments(account, ref).isEmpty()) {
1380 0 : draftsByUser.put(account, ref.getObjectId());
1381 : }
1382 0 : }
1383 : }
1384 3 : return draftsByUser;
1385 : }
1386 : }
|