Line data Source code
1 : // Copyright (C) 2013 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.notedb;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.base.Preconditions.checkState;
19 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
20 : import static com.google.gerrit.entities.RefNames.changeMetaRef;
21 : import static java.util.Comparator.comparing;
22 :
23 : import com.google.auto.value.AutoValue;
24 : import com.google.common.annotations.VisibleForTesting;
25 : import com.google.common.base.Strings;
26 : import com.google.common.collect.ImmutableList;
27 : import com.google.common.collect.ImmutableListMultimap;
28 : import com.google.common.collect.ImmutableSet;
29 : import com.google.common.collect.ImmutableSortedMap;
30 : import com.google.common.collect.ImmutableSortedSet;
31 : import com.google.common.collect.ListMultimap;
32 : import com.google.common.collect.Lists;
33 : import com.google.common.collect.Multimaps;
34 : import com.google.common.collect.Ordering;
35 : import com.google.common.collect.Sets;
36 : import com.google.common.collect.Sets.SetView;
37 : import com.google.common.flogger.FluentLogger;
38 : import com.google.errorprone.annotations.FormatMethod;
39 : import com.google.gerrit.common.Nullable;
40 : import com.google.gerrit.entities.Account;
41 : import com.google.gerrit.entities.AttentionSetUpdate;
42 : import com.google.gerrit.entities.BranchNameKey;
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.PatchSet;
48 : import com.google.gerrit.entities.PatchSetApproval;
49 : import com.google.gerrit.entities.PatchSetApprovals;
50 : import com.google.gerrit.entities.Project;
51 : import com.google.gerrit.entities.RefNames;
52 : import com.google.gerrit.entities.RobotComment;
53 : import com.google.gerrit.entities.SubmitRecord;
54 : import com.google.gerrit.entities.SubmitRequirementResult;
55 : import com.google.gerrit.server.AssigneeStatusUpdate;
56 : import com.google.gerrit.server.ReviewerByEmailSet;
57 : import com.google.gerrit.server.ReviewerSet;
58 : import com.google.gerrit.server.ReviewerStatusUpdate;
59 : import com.google.gerrit.server.git.RefCache;
60 : import com.google.gerrit.server.project.NoSuchChangeException;
61 : import com.google.gerrit.server.project.ProjectCache;
62 : import com.google.gerrit.server.query.change.ChangeData;
63 : import com.google.gerrit.server.query.change.InternalChangeQuery;
64 : import com.google.inject.Inject;
65 : import com.google.inject.Provider;
66 : import com.google.inject.Singleton;
67 : import java.io.IOException;
68 : import java.time.Instant;
69 : import java.util.ArrayList;
70 : import java.util.Collection;
71 : import java.util.Collections;
72 : import java.util.List;
73 : import java.util.Objects;
74 : import java.util.Optional;
75 : import java.util.function.Predicate;
76 : import java.util.stream.Stream;
77 : import org.eclipse.jgit.errors.ConfigInvalidException;
78 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
79 : import org.eclipse.jgit.lib.ObjectId;
80 : import org.eclipse.jgit.lib.Ref;
81 : import org.eclipse.jgit.lib.Repository;
82 :
83 : /** View of a single {@link Change} based on the log of its notes branch. */
84 : // TODO(paiking): This class should be refactored to get rid of potentially duplicate or unneeded
85 : // variables, such as allAttentionSetUpdates, reviewerUpdates, and others.
86 :
87 : public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
88 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
89 :
90 152 : static final Ordering<PatchSetApproval> PSA_BY_TIME =
91 152 : Ordering.from(comparing(PatchSetApproval::granted));
92 :
93 : @FormatMethod
94 : public static ConfigInvalidException parseException(
95 : Change.Id changeId, String fmt, Object... args) {
96 1 : return new ConfigInvalidException("Change " + changeId + ": " + String.format(fmt, args));
97 : }
98 :
99 : @Singleton
100 : public static class Factory {
101 : private final Args args;
102 : private final Provider<InternalChangeQuery> queryProvider;
103 : private final ProjectCache projectCache;
104 :
105 : @VisibleForTesting
106 : @Inject
107 : public Factory(
108 152 : Args args, Provider<InternalChangeQuery> queryProvider, ProjectCache projectCache) {
109 152 : this.args = args;
110 152 : this.queryProvider = queryProvider;
111 152 : this.projectCache = projectCache;
112 152 : }
113 :
114 : @AutoValue
115 16 : public abstract static class ScanResult {
116 : abstract ImmutableSet<Change.Id> fromPatchSetRefs();
117 :
118 : abstract ImmutableSet<Change.Id> fromMetaRefs();
119 :
120 : public SetView<Change.Id> all() {
121 16 : return Sets.union(fromPatchSetRefs(), fromMetaRefs());
122 : }
123 : }
124 :
125 : public static ScanResult scanChangeIds(Repository repo) throws IOException {
126 16 : ImmutableSet.Builder<Change.Id> fromPs = ImmutableSet.builder();
127 16 : ImmutableSet.Builder<Change.Id> fromMeta = ImmutableSet.builder();
128 16 : for (Ref r : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
129 3 : Change.Id id = Change.Id.fromRef(r.getName());
130 3 : if (id != null) {
131 3 : (r.getName().endsWith(RefNames.META_SUFFIX) ? fromMeta : fromPs).add(id);
132 : }
133 3 : }
134 16 : return new AutoValue_ChangeNotes_Factory_ScanResult(fromPs.build(), fromMeta.build());
135 : }
136 :
137 : public ChangeNotes createChecked(Change c) {
138 96 : return createChecked(c.getProject(), c.getId());
139 : }
140 :
141 : public ChangeNotes createChecked(
142 : Project.NameKey project, Change.Id changeId, @Nullable ObjectId metaRevId) {
143 103 : Change change = newChange(project, changeId);
144 103 : return new ChangeNotes(args, change, true, null, metaRevId).load();
145 : }
146 :
147 : public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) {
148 103 : return createChecked(project, changeId, null);
149 : }
150 :
151 : public static Change newChange(Project.NameKey project, Change.Id changeId) {
152 103 : return new Change(
153 103 : null, changeId, null, BranchNameKey.create(project, "INVALID_NOTE_DB_ONLY"), null);
154 : }
155 :
156 : public ChangeNotes create(Project.NameKey project, Change.Id changeId) {
157 99 : checkArgument(project != null, "project is required");
158 99 : return new ChangeNotes(args, newChange(project, changeId), true, null).load();
159 : }
160 :
161 : public ChangeNotes create(Repository repository, Project.NameKey project, Change.Id changeId) {
162 42 : checkArgument(project != null, "project is required");
163 42 : return new ChangeNotes(args, newChange(project, changeId), true, null).load(repository);
164 : }
165 :
166 : /**
167 : * Create change notes for a change that was loaded from index. This method should only be used
168 : * when database access is harmful and potentially stale data from the index is acceptable.
169 : *
170 : * @param change change loaded from secondary index
171 : * @return change notes
172 : */
173 : public ChangeNotes createFromIndexedChange(Change change) {
174 0 : return new ChangeNotes(args, change, true, null);
175 : }
176 :
177 : public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist) {
178 103 : return new ChangeNotes(args, change, shouldExist, null).load();
179 : }
180 :
181 : public ChangeNotes create(Change change, RefCache refs) {
182 0 : return new ChangeNotes(args, change, true, refs).load();
183 : }
184 :
185 : /**
186 : * Create change notes based on a {@link com.google.gerrit.entities.Change.Id}. This requires
187 : * using the Change index and should only be used when {@link
188 : * com.google.gerrit.entities.Project.NameKey} and the numeric change ID are not available.
189 : */
190 : public ChangeNotes createCheckedUsingIndexLookup(Change.Id changeId) {
191 2 : InternalChangeQuery query = queryProvider.get().setLimit(2).noFields();
192 2 : List<ChangeData> changes = query.byLegacyChangeId(changeId);
193 2 : if (changes.isEmpty()) {
194 0 : throw new NoSuchChangeException(changeId);
195 : }
196 2 : if (changes.size() != 1) {
197 0 : logger.atSevere().log("Multiple changes found for %d", changeId.get());
198 0 : throw new NoSuchChangeException(changeId);
199 : }
200 2 : return changes.get(0).notes();
201 : }
202 :
203 : /**
204 : * Create change notes based on a list of {@link com.google.gerrit.entities.Change.Id}s. This
205 : * requires using the Change index and should only be used when {@link
206 : * com.google.gerrit.entities.Project.NameKey} and the numeric change ID are not available.
207 : */
208 : public List<ChangeNotes> createUsingIndexLookup(Collection<Change.Id> changeIds) {
209 1 : List<ChangeNotes> notes = new ArrayList<>();
210 1 : for (Change.Id changeId : changeIds) {
211 : try {
212 1 : notes.add(createCheckedUsingIndexLookup(changeId));
213 0 : } catch (NoSuchChangeException e) {
214 : // Ignore missing changes to match Access#get(Iterable) behavior.
215 1 : }
216 1 : }
217 1 : return notes;
218 : }
219 :
220 : public List<ChangeNotes> create(
221 : Repository repo,
222 : Project.NameKey project,
223 : Collection<Change.Id> changeIds,
224 : Predicate<ChangeNotes> predicate) {
225 4 : List<ChangeNotes> notes = new ArrayList<>();
226 4 : for (Change.Id cid : changeIds) {
227 : try {
228 4 : ChangeNotes cn = create(repo, project, cid);
229 4 : if (cn.getChange() != null && predicate.test(cn)) {
230 4 : notes.add(cn);
231 : }
232 4 : } catch (NoSuchChangeException e) {
233 : // Match ReviewDb behavior, returning not found; maybe the caller learned about it from
234 : // a dangling patch set ref or something.
235 4 : continue;
236 4 : }
237 4 : }
238 4 : return notes;
239 : }
240 :
241 : /* TODO: This is now unused in the Gerrit code-base, however it is kept in the code
242 : /* because it is a public method in a stable branch.
243 : * It can be removed in master branch where we have more flexibility to change the API
244 : * interface.
245 : */
246 : public List<ChangeNotes> create(
247 : Project.NameKey project,
248 : Collection<Change.Id> changeIds,
249 : Predicate<ChangeNotes> predicate) {
250 0 : try (Repository repo = args.repoManager.openRepository(project)) {
251 0 : return create(repo, project, changeIds, predicate);
252 0 : } catch (RepositoryNotFoundException e) {
253 : // The repository does not exist, hence it does not contain
254 : // any change.
255 0 : } catch (IOException e) {
256 0 : logger.atWarning().withCause(e).log(
257 : "Unable to open project=%s when trying to retrieve changeId=%s from NoteDb",
258 : project, changeIds);
259 0 : }
260 0 : return Collections.emptyList();
261 : }
262 :
263 : public ListMultimap<Project.NameKey, ChangeNotes> create(Predicate<ChangeNotes> predicate)
264 : throws IOException {
265 : ImmutableListMultimap.Builder<Project.NameKey, ChangeNotes> m =
266 0 : ImmutableListMultimap.builder();
267 0 : for (Project.NameKey project : projectCache.all()) {
268 0 : try (Repository repo = args.repoManager.openRepository(project)) {
269 0 : scan(repo, project)
270 0 : .filter(r -> !r.error().isPresent())
271 0 : .map(ChangeNotesResult::notes)
272 0 : .filter(predicate)
273 0 : .forEach(n -> m.put(n.getProjectName(), n));
274 : }
275 0 : }
276 0 : return m.build();
277 : }
278 :
279 : public Stream<ChangeNotesResult> scan(Repository repo, Project.NameKey project)
280 : throws IOException {
281 0 : return scan(repo, project, null);
282 : }
283 :
284 : public Stream<ChangeNotesResult> scan(
285 : Repository repo, Project.NameKey project, Predicate<Change.Id> changeIdPredicate)
286 : throws IOException {
287 0 : return scan(scanChangeIds(repo), project, changeIdPredicate);
288 : }
289 :
290 : public Stream<ChangeNotesResult> scan(
291 : ScanResult sr, Project.NameKey project, Predicate<Change.Id> changeIdPredicate) {
292 3 : Stream<Change.Id> idStream = sr.all().stream();
293 3 : if (changeIdPredicate != null) {
294 3 : idStream = idStream.filter(changeIdPredicate);
295 : }
296 3 : return idStream.map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
297 : }
298 :
299 : @Nullable
300 : private ChangeNotesResult scanOneChange(Project.NameKey project, ScanResult sr, Change.Id id) {
301 3 : if (!sr.fromMetaRefs().contains(id)) {
302 : // Stray patch set refs can happen due to normal error conditions, e.g. failed
303 : // push processing, so aren't worth even a warning.
304 0 : return null;
305 : }
306 :
307 : // TODO(dborowitz): See discussion in BatchUpdate#newChangeContext.
308 : try {
309 3 : Change change = ChangeNotes.Factory.newChange(project, id);
310 3 : logger.atFine().log("adding change %s found in project %s", id, project);
311 3 : return toResult(change);
312 0 : } catch (InvalidServerIdException ise) {
313 0 : logger.atWarning().withCause(ise).log(
314 0 : "skipping change %d in project %s because of an invalid server id", id.get(), project);
315 0 : return null;
316 : }
317 : }
318 :
319 : @Nullable
320 : private ChangeNotesResult toResult(Change rawChangeFromNoteDb) {
321 3 : ChangeNotes n = new ChangeNotes(args, rawChangeFromNoteDb, true, null);
322 : try {
323 3 : n.load();
324 0 : } catch (Exception e) {
325 0 : return ChangeNotesResult.error(n.getChangeId(), e);
326 3 : }
327 3 : return ChangeNotesResult.notes(n);
328 : }
329 :
330 : /** Result of {@link #scan(Repository,Project.NameKey)}. */
331 : @AutoValue
332 3 : public abstract static class ChangeNotesResult {
333 : static ChangeNotesResult error(Change.Id id, Throwable e) {
334 0 : return new AutoValue_ChangeNotes_Factory_ChangeNotesResult(id, Optional.of(e), null);
335 : }
336 :
337 : static ChangeNotesResult notes(ChangeNotes notes) {
338 3 : return new AutoValue_ChangeNotes_Factory_ChangeNotesResult(
339 3 : notes.getChangeId(), Optional.empty(), notes);
340 : }
341 :
342 : /** Change ID that was scanned. */
343 : public abstract Change.Id id();
344 :
345 : /** Error encountered while loading this change, if any. */
346 : public abstract Optional<Throwable> error();
347 :
348 : /**
349 : * Notes loaded for this change.
350 : *
351 : * @return notes.
352 : * @throws IllegalStateException if there was an error loading the change; callers must check
353 : * that {@link #error()} is absent before attempting to look up the notes.
354 : */
355 : public ChangeNotes notes() {
356 3 : checkState(maybeNotes() != null, "no ChangeNotes loaded; check error().isPresent() first");
357 3 : return maybeNotes();
358 : }
359 :
360 : @Nullable
361 : abstract ChangeNotes maybeNotes();
362 : }
363 : }
364 :
365 : private final boolean shouldExist;
366 : private final RefCache refs;
367 :
368 : private Change change;
369 : private ChangeNotesState state;
370 :
371 : // Parsed note map state, used by ChangeUpdate to make in-place editing of
372 : // notes easier.
373 : RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
374 :
375 : private DraftCommentNotes draftCommentNotes;
376 : private RobotCommentNotes robotCommentNotes;
377 :
378 : // Lazy defensive copies of mutable ReviewDb types, to avoid polluting the
379 : // ChangeNotesCache from handlers.
380 : private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
381 : private PatchSetApprovals approvals;
382 : private ImmutableSet<Comment.Key> commentKeys;
383 :
384 : public ChangeNotes(
385 : Args args,
386 : Change change,
387 : boolean shouldExist,
388 : @Nullable RefCache refs,
389 : @Nullable ObjectId metaSha1) {
390 103 : super(args, change.getId(), metaSha1);
391 103 : this.change = new Change(change);
392 103 : this.shouldExist = shouldExist;
393 103 : this.refs = refs;
394 103 : }
395 :
396 : @VisibleForTesting
397 : public ChangeNotes(Args args, Change change, boolean shouldExist, @Nullable RefCache refs) {
398 103 : this(args, change, shouldExist, refs, null);
399 103 : }
400 :
401 : public Change getChange() {
402 103 : return change;
403 : }
404 :
405 : public ObjectId getMetaId() {
406 103 : return state.metaId();
407 : }
408 :
409 : public String getServerId() {
410 1 : return state.serverId();
411 : }
412 :
413 : public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
414 103 : if (patchSets == null) {
415 103 : ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b = ImmutableSortedMap.naturalOrder();
416 103 : b.putAll(state.patchSets());
417 103 : patchSets = b.build();
418 : }
419 103 : return patchSets;
420 : }
421 :
422 : /** Gets the approvals of all patch sets. */
423 : public PatchSetApprovals getApprovals() {
424 103 : if (approvals == null) {
425 103 : approvals = PatchSetApprovals.create(ImmutableListMultimap.copyOf(state.approvals()));
426 : }
427 103 : return approvals;
428 : }
429 :
430 : public ReviewerSet getReviewers() {
431 103 : return state.reviewers();
432 : }
433 :
434 : /** Returns reviewers that do not currently have a Gerrit account and were added by email. */
435 : public ReviewerByEmailSet getReviewersByEmail() {
436 103 : return state.reviewersByEmail();
437 : }
438 :
439 : /** Returns reviewers that were modified during this change's current WIP phase. */
440 : public ReviewerSet getPendingReviewers() {
441 103 : return state.pendingReviewers();
442 : }
443 :
444 : /** Returns reviewers by email that were modified during this change's current WIP phase. */
445 : public ReviewerByEmailSet getPendingReviewersByEmail() {
446 103 : return state.pendingReviewersByEmail();
447 : }
448 :
449 : public ImmutableList<ReviewerStatusUpdate> getReviewerUpdates() {
450 103 : return state.reviewerUpdates();
451 : }
452 :
453 : /** Returns the most recent update (i.e. status) per user. */
454 : public ImmutableSet<AttentionSetUpdate> getAttentionSet() {
455 103 : return state.attentionSet();
456 : }
457 :
458 : /** Returns all updates for the attention set. */
459 : public ImmutableList<AttentionSetUpdate> getAttentionSetUpdates() {
460 1 : return state.allAttentionSetUpdates();
461 : }
462 :
463 : /**
464 : * Returns the evaluated submit requirements for the change. We only intend to store submit
465 : * requirements in NoteDb for closed changes. For closed changes, the results represent the state
466 : * of evaluating submit requirements for this change when it was merged or abandoned.
467 : *
468 : * @throws UnsupportedOperationException if submit requirements are requested for an open change.
469 : */
470 : public ImmutableList<SubmitRequirementResult> getSubmitRequirementsResult() {
471 57 : if (state.columns().status().isOpen()) {
472 0 : throw new UnsupportedOperationException(
473 0 : String.format(
474 : "Cannot request stored submit requirements"
475 : + " for an open change: project = %s, change ID = %d",
476 0 : getProjectName(), state.changeId().get()));
477 : }
478 57 : return state.submitRequirementsResult();
479 : }
480 :
481 : /**
482 : * Returns an ImmutableSet of Account.Ids of all users that have been assigned to this change. The
483 : * order of the set is the order in which they were assigned.
484 : */
485 : public ImmutableSet<Account.Id> getPastAssignees() {
486 3 : return Lists.reverse(state.assigneeUpdates()).stream()
487 3 : .map(AssigneeStatusUpdate::currentAssignee)
488 3 : .filter(Optional::isPresent)
489 3 : .map(Optional::get)
490 3 : .collect(toImmutableSet());
491 : }
492 :
493 : /**
494 : * Returns an ImmutableList of AssigneeStatusUpdate of all the updates to the assignee field to
495 : * this change. The order of the list is from most recent updates to least recent.
496 : */
497 : public ImmutableList<AssigneeStatusUpdate> getAssigneeUpdates() {
498 1 : return state.assigneeUpdates();
499 : }
500 :
501 : /** Returns an ImmutableSet of all hashtags for this change sorted in alphabetical order. */
502 : public ImmutableSet<String> getHashtags() {
503 103 : return ImmutableSortedSet.copyOf(state.hashtags());
504 : }
505 :
506 : /** Returns a list of all users who have ever been a reviewer on this change. */
507 : public ImmutableList<Account.Id> getAllPastReviewers() {
508 1 : return state.allPastReviewers();
509 : }
510 :
511 : /**
512 : * Returns submit records stored during the most recent submit; only for changes that were
513 : * actually submitted.
514 : */
515 : public ImmutableList<SubmitRecord> getSubmitRecords() {
516 58 : return state.submitRecords();
517 : }
518 :
519 : /** Returns all change messages, in chronological order, oldest first. */
520 : public ImmutableList<ChangeMessage> getChangeMessages() {
521 103 : return state.changeMessages();
522 : }
523 :
524 : /** Returns inline comments on each revision. */
525 : public ImmutableListMultimap<ObjectId, HumanComment> getHumanComments() {
526 103 : return state.publishedComments();
527 : }
528 :
529 : public ImmutableSet<Comment.Key> getCommentKeys() {
530 21 : if (commentKeys == null) {
531 21 : ImmutableSet.Builder<Comment.Key> b = ImmutableSet.builder();
532 21 : for (Comment c : getHumanComments().values()) {
533 10 : b.add(new Comment.Key(c.key));
534 10 : }
535 21 : commentKeys = b.build();
536 : }
537 21 : return commentKeys;
538 : }
539 :
540 : public int getUpdateCount() {
541 103 : return state.updateCount();
542 : }
543 :
544 : /** Returns {@link Optional} value of time when the change was merged. */
545 : public Optional<Instant> getMergedOn() {
546 103 : return Optional.ofNullable(state.mergedOn());
547 : }
548 :
549 : public ImmutableListMultimap<ObjectId, HumanComment> getDraftComments(Account.Id author) {
550 29 : return getDraftComments(author, null);
551 : }
552 :
553 : public ImmutableListMultimap<ObjectId, HumanComment> getDraftComments(
554 : Account.Id author, @Nullable Ref ref) {
555 29 : loadDraftComments(author, ref);
556 : // Filter out any zombie draft comments. These are drafts that are also in
557 : // the published map, and arise when the update to All-Users to delete them
558 : // during the publish operation failed.
559 29 : return ImmutableListMultimap.copyOf(
560 29 : Multimaps.filterEntries(
561 29 : draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key)));
562 : }
563 :
564 : public ImmutableListMultimap<ObjectId, RobotComment> getRobotComments() {
565 103 : loadRobotComments();
566 103 : return robotCommentNotes.getComments();
567 : }
568 :
569 : /**
570 : * If draft comments have already been loaded for this author, then they will not be reloaded.
571 : * However, this method will load the comments if no draft comments have been loaded or if the
572 : * caller would like the drafts for another author.
573 : */
574 : private void loadDraftComments(Account.Id author, @Nullable Ref ref) {
575 29 : if (draftCommentNotes == null || !author.equals(draftCommentNotes.getAuthor()) || ref != null) {
576 29 : draftCommentNotes = new DraftCommentNotes(args, getChangeId(), author, ref);
577 29 : draftCommentNotes.load();
578 : }
579 29 : }
580 :
581 : private void loadRobotComments() {
582 103 : if (robotCommentNotes == null) {
583 103 : robotCommentNotes = new RobotCommentNotes(args, change);
584 103 : robotCommentNotes.load();
585 : }
586 103 : }
587 :
588 : @VisibleForTesting
589 : DraftCommentNotes getDraftCommentNotes() {
590 21 : return draftCommentNotes;
591 : }
592 :
593 : public RobotCommentNotes getRobotCommentNotes() {
594 103 : loadRobotComments();
595 103 : return robotCommentNotes;
596 : }
597 :
598 : public boolean containsComment(HumanComment c) {
599 0 : if (containsCommentPublished(c)) {
600 0 : return true;
601 : }
602 0 : loadDraftComments(c.author.getId(), null);
603 0 : return draftCommentNotes.containsComment(c);
604 : }
605 :
606 : public boolean containsCommentPublished(Comment c) {
607 0 : for (Comment l : getHumanComments().values()) {
608 0 : if (c.key.equals(l.key)) {
609 0 : return true;
610 : }
611 0 : }
612 0 : return false;
613 : }
614 :
615 : @Override
616 : public String getRefName() {
617 103 : return changeMetaRef(getChangeId());
618 : }
619 :
620 : public PatchSet getCurrentPatchSet() {
621 81 : PatchSet.Id psId = change.currentPatchSetId();
622 81 : if (psId == null || getPatchSets().get(psId) == null) {
623 : // In some cases, the current patch-set doesn't exist yet as it's being created during the
624 : // operation (e.g rebase).
625 10 : PatchSet currentPatchset =
626 10 : getPatchSets().values().stream()
627 10 : .max((p1, p2) -> p1.id().get() - p2.id().get())
628 10 : .orElseThrow(
629 : () ->
630 0 : new IllegalStateException(
631 0 : String.format(
632 0 : "change %s can't load any patchset", getChangeId().toString())));
633 10 : return currentPatchset;
634 : }
635 81 : return getPatchSets().get(psId);
636 : }
637 :
638 : @Override
639 : protected void onLoad(LoadHandle handle) throws NoSuchChangeException, IOException {
640 103 : ObjectId rev = handle.id();
641 103 : if (rev == null) {
642 103 : if (shouldExist) {
643 14 : throw new NoSuchChangeException(getChangeId());
644 : }
645 103 : loadDefaults();
646 103 : return;
647 : }
648 :
649 103 : ChangeNotesCache.Value v =
650 103 : args.cache.get().get(getProjectName(), getChangeId(), rev, handle::walk);
651 103 : state = v.state();
652 :
653 103 : String stateServerId = state.serverId();
654 : /**
655 : * In earlier Gerrit versions serverId wasn't part of the change notes cache. That's why the
656 : * earlier cached entries don't have the serverId attribute. That's fine because in earlier
657 : * gerrit version serverId was already validated. Another approach to simplify the check would
658 : * be to bump the cache version, but that would invalidate all persistent cache entries, what we
659 : * rather try to avoid.
660 : */
661 103 : if (!Strings.isNullOrEmpty(stateServerId)
662 103 : && !args.serverId.equals(stateServerId)
663 1 : && !args.importedServerIds.contains(stateServerId)) {
664 1 : throw new InvalidServerIdException(args.serverId, stateServerId);
665 : }
666 :
667 103 : state.copyColumnsTo(change);
668 103 : revisionNoteMap = v.revisionNoteMap();
669 103 : }
670 :
671 : @Override
672 : protected void loadDefaults() {
673 103 : state = ChangeNotesState.empty(change);
674 103 : }
675 :
676 : @Override
677 : public Project.NameKey getProjectName() {
678 103 : return change.getProject();
679 : }
680 :
681 : @Nullable
682 : @Override
683 : protected ObjectId readRef(Repository repo) throws IOException {
684 103 : return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo);
685 : }
686 : }
|