| // Copyright (C) 2013 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.notedb; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap; |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.gerrit.entities.RefNames.changeMetaRef; |
| import static java.util.Comparator.comparing; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableListMultimap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.MultimapBuilder; |
| import com.google.common.collect.Multimaps; |
| import com.google.common.collect.Ordering; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.Sets.SetView; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.errorprone.annotations.FormatMethod; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.AttentionSetUpdate; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.ChangeMessage; |
| import com.google.gerrit.entities.Comment; |
| import com.google.gerrit.entities.HumanComment; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.PatchSetApproval; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.entities.RobotComment; |
| import com.google.gerrit.entities.SubmitRecord; |
| import com.google.gerrit.entities.SubmitRequirementResult; |
| import com.google.gerrit.server.AssigneeStatusUpdate; |
| import com.google.gerrit.server.ReviewerByEmailSet; |
| import com.google.gerrit.server.ReviewerSet; |
| import com.google.gerrit.server.ReviewerStatusUpdate; |
| import com.google.gerrit.server.git.RefCache; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.InternalChangeQuery; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.function.Predicate; |
| import java.util.stream.Stream; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** View of a single {@link Change} based on the log of its notes branch. */ |
| // TODO(paiking): This class should be refactored to get rid of potentially duplicate or unneeded |
| // variables, such as allAttentionSetUpdates, reviewerUpdates, and others. |
| |
| public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| static final Ordering<PatchSetApproval> PSA_BY_TIME = |
| Ordering.from(comparing(PatchSetApproval::granted)); |
| |
| public static final Ordering<ChangeMessage> MESSAGE_BY_TIME = |
| Ordering.from(comparing(ChangeMessage::getWrittenOn)); |
| |
| @FormatMethod |
| public static ConfigInvalidException parseException( |
| Change.Id changeId, String fmt, Object... args) { |
| return new ConfigInvalidException("Change " + changeId + ": " + String.format(fmt, args)); |
| } |
| |
| @Singleton |
| public static class Factory { |
| private final Args args; |
| private final Provider<InternalChangeQuery> queryProvider; |
| private final ProjectCache projectCache; |
| |
| @VisibleForTesting |
| @Inject |
| public Factory( |
| Args args, Provider<InternalChangeQuery> queryProvider, ProjectCache projectCache) { |
| this.args = args; |
| this.queryProvider = queryProvider; |
| this.projectCache = projectCache; |
| } |
| |
| @AutoValue |
| public abstract static class ScanResult { |
| abstract ImmutableSet<Change.Id> fromPatchSetRefs(); |
| |
| abstract ImmutableSet<Change.Id> fromMetaRefs(); |
| |
| public SetView<Change.Id> all() { |
| return Sets.union(fromPatchSetRefs(), fromMetaRefs()); |
| } |
| } |
| |
| public static ScanResult scanChangeIds(Repository repo) throws IOException { |
| ImmutableSet.Builder<Change.Id> fromPs = ImmutableSet.builder(); |
| ImmutableSet.Builder<Change.Id> fromMeta = ImmutableSet.builder(); |
| for (Ref r : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) { |
| Change.Id id = Change.Id.fromRef(r.getName()); |
| if (id != null) { |
| (r.getName().endsWith(RefNames.META_SUFFIX) ? fromMeta : fromPs).add(id); |
| } |
| } |
| return new AutoValue_ChangeNotes_Factory_ScanResult(fromPs.build(), fromMeta.build()); |
| } |
| |
| public ChangeNotes createChecked(Change c) { |
| return createChecked(c.getProject(), c.getId()); |
| } |
| |
| public ChangeNotes createChecked( |
| Repository repo, |
| Project.NameKey project, |
| Change.Id changeId, |
| @Nullable ObjectId metaRevId) { |
| Change change = newChange(project, changeId); |
| return new ChangeNotes(args, change, true, null, metaRevId).load(repo); |
| } |
| |
| public ChangeNotes createChecked( |
| Project.NameKey project, Change.Id changeId, @Nullable ObjectId metaRevId) { |
| Change change = newChange(project, changeId); |
| return new ChangeNotes(args, change, true, null, metaRevId).load(); |
| } |
| |
| public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) { |
| return createChecked(project, changeId, null); |
| } |
| |
| public static Change newChange(Project.NameKey project, Change.Id changeId) { |
| return new Change( |
| null, changeId, null, BranchNameKey.create(project, "INVALID_NOTE_DB_ONLY"), null); |
| } |
| |
| public ChangeNotes create(Project.NameKey project, Change.Id changeId) { |
| checkArgument(project != null, "project is required"); |
| return new ChangeNotes(args, newChange(project, changeId), true, null).load(); |
| } |
| |
| public ChangeNotes create(Repository repository, Project.NameKey project, Change.Id changeId) { |
| checkArgument(project != null, "project is required"); |
| return new ChangeNotes(args, newChange(project, changeId), true, null).load(repository); |
| } |
| |
| /** |
| * Create change notes for a change that was loaded from index. This method should only be used |
| * when database access is harmful and potentially stale data from the index is acceptable. |
| * |
| * @param change change loaded from secondary index |
| * @return change notes |
| */ |
| public ChangeNotes createFromIndexedChange(Change change) { |
| return new ChangeNotes(args, change, true, null); |
| } |
| |
| public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist) { |
| return new ChangeNotes(args, change, shouldExist, null).load(); |
| } |
| |
| public ChangeNotes create(Change change, RefCache refs) { |
| return new ChangeNotes(args, change, true, refs).load(); |
| } |
| |
| /** |
| * Create change notes based on a {@link com.google.gerrit.entities.Change.Id}. This requires |
| * using the Change index and should only be used when {@link |
| * com.google.gerrit.entities.Project.NameKey} and the numeric change ID are not available. |
| */ |
| public ChangeNotes createCheckedUsingIndexLookup(Change.Id changeId) { |
| InternalChangeQuery query = queryProvider.get().noFields(); |
| List<ChangeData> changes = query.byLegacyChangeId(changeId); |
| if (changes.isEmpty()) { |
| throw new NoSuchChangeException(changeId); |
| } |
| if (changes.size() != 1) { |
| logger.atSevere().log("Multiple changes found for %d", changeId.get()); |
| throw new NoSuchChangeException(changeId); |
| } |
| return changes.get(0).notes(); |
| } |
| |
| /** |
| * Create change notes based on a list of {@link com.google.gerrit.entities.Change.Id}s. This |
| * requires using the Change index and should only be used when {@link |
| * com.google.gerrit.entities.Project.NameKey} and the numeric change ID are not available. |
| */ |
| public List<ChangeNotes> createUsingIndexLookup(Collection<Change.Id> changeIds) { |
| List<ChangeNotes> notes = new ArrayList<>(); |
| for (Change.Id changeId : changeIds) { |
| try { |
| notes.add(createCheckedUsingIndexLookup(changeId)); |
| } catch (NoSuchChangeException e) { |
| // Ignore missing changes to match Access#get(Iterable) behavior. |
| } |
| } |
| return notes; |
| } |
| |
| public List<ChangeNotes> create( |
| Repository repo, |
| Project.NameKey project, |
| Collection<Change.Id> changeIds, |
| Predicate<ChangeNotes> predicate) { |
| List<ChangeNotes> notes = new ArrayList<>(); |
| for (Change.Id cid : changeIds) { |
| try { |
| ChangeNotes cn = create(repo, project, cid); |
| if (cn.getChange() != null && predicate.test(cn)) { |
| notes.add(cn); |
| } |
| } catch (NoSuchChangeException e) { |
| // Match ReviewDb behavior, returning not found; maybe the caller learned about it from |
| // a dangling patch set ref or something. |
| continue; |
| } |
| } |
| return notes; |
| } |
| |
| /* TODO: This is now unused in the Gerrit code-base, however it is kept in the code |
| /* because it is a public method in a stable branch. |
| * It can be removed in master branch where we have more flexibility to change the API |
| * interface. |
| */ |
| public List<ChangeNotes> create( |
| Project.NameKey project, |
| Collection<Change.Id> changeIds, |
| Predicate<ChangeNotes> predicate) { |
| try (Repository repo = args.repoManager.openRepository(project)) { |
| return create(repo, project, changeIds, predicate); |
| } catch (RepositoryNotFoundException e) { |
| // The repository does not exist, hence it does not contain |
| // any change. |
| } catch (IOException e) { |
| logger.atWarning().withCause(e).log( |
| "Unable to open project=%s when trying to retrieve changeId=%s from NoteDb", |
| project, changeIds); |
| } |
| return Collections.emptyList(); |
| } |
| |
| public ListMultimap<Project.NameKey, ChangeNotes> create(Predicate<ChangeNotes> predicate) |
| throws IOException { |
| ListMultimap<Project.NameKey, ChangeNotes> m = |
| MultimapBuilder.hashKeys().arrayListValues().build(); |
| for (Project.NameKey project : projectCache.all()) { |
| try (Repository repo = args.repoManager.openRepository(project)) { |
| scan(repo, project) |
| .filter(r -> !r.error().isPresent()) |
| .map(ChangeNotesResult::notes) |
| .filter(predicate) |
| .forEach(n -> m.put(n.getProjectName(), n)); |
| } |
| } |
| return ImmutableListMultimap.copyOf(m); |
| } |
| |
| public Stream<ChangeNotesResult> scan(Repository repo, Project.NameKey project) |
| throws IOException { |
| return scan(repo, project, null); |
| } |
| |
| public Stream<ChangeNotesResult> scan( |
| Repository repo, Project.NameKey project, Predicate<Change.Id> changeIdPredicate) |
| throws IOException { |
| return scan(scanChangeIds(repo), project, changeIdPredicate); |
| } |
| |
| public Stream<ChangeNotesResult> scan( |
| ScanResult sr, Project.NameKey project, Predicate<Change.Id> changeIdPredicate) { |
| Stream<Change.Id> idStream = sr.all().stream(); |
| if (changeIdPredicate != null) { |
| idStream = idStream.filter(changeIdPredicate); |
| } |
| return idStream.map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull); |
| } |
| |
| @Nullable |
| private ChangeNotesResult scanOneChange(Project.NameKey project, ScanResult sr, Change.Id id) { |
| if (!sr.fromMetaRefs().contains(id)) { |
| // Stray patch set refs can happen due to normal error conditions, e.g. failed |
| // push processing, so aren't worth even a warning. |
| return null; |
| } |
| |
| // TODO(dborowitz): See discussion in BatchUpdate#newChangeContext. |
| try { |
| Change change = ChangeNotes.Factory.newChange(project, id); |
| logger.atFine().log("adding change %s found in project %s", id, project); |
| return toResult(change); |
| } catch (InvalidServerIdException ise) { |
| logger.atWarning().withCause(ise).log( |
| "skipping change %d in project %s because of an invalid server id", id.get(), project); |
| return null; |
| } |
| } |
| |
| @Nullable |
| private ChangeNotesResult toResult(Change rawChangeFromNoteDb) { |
| ChangeNotes n = new ChangeNotes(args, rawChangeFromNoteDb, true, null); |
| try { |
| n.load(); |
| } catch (Exception e) { |
| return ChangeNotesResult.error(n.getChangeId(), e); |
| } |
| return ChangeNotesResult.notes(n); |
| } |
| |
| /** Result of {@link #scan(Repository,Project.NameKey)}. */ |
| @AutoValue |
| public abstract static class ChangeNotesResult { |
| static ChangeNotesResult error(Change.Id id, Throwable e) { |
| return new AutoValue_ChangeNotes_Factory_ChangeNotesResult(id, Optional.of(e), null); |
| } |
| |
| static ChangeNotesResult notes(ChangeNotes notes) { |
| return new AutoValue_ChangeNotes_Factory_ChangeNotesResult( |
| notes.getChangeId(), Optional.empty(), notes); |
| } |
| |
| /** Change ID that was scanned. */ |
| public abstract Change.Id id(); |
| |
| /** Error encountered while loading this change, if any. */ |
| public abstract Optional<Throwable> error(); |
| |
| /** |
| * Notes loaded for this change. |
| * |
| * @return notes. |
| * @throws IllegalStateException if there was an error loading the change; callers must check |
| * that {@link #error()} is absent before attempting to look up the notes. |
| */ |
| public ChangeNotes notes() { |
| checkState(maybeNotes() != null, "no ChangeNotes loaded; check error().isPresent() first"); |
| return maybeNotes(); |
| } |
| |
| @Nullable |
| abstract ChangeNotes maybeNotes(); |
| } |
| } |
| |
| private final boolean shouldExist; |
| private final RefCache refs; |
| |
| private Change change; |
| private ChangeNotesState state; |
| |
| // Parsed note map state, used by ChangeUpdate to make in-place editing of |
| // notes easier. |
| RevisionNoteMap<ChangeRevisionNote> revisionNoteMap; |
| |
| private DraftCommentNotes draftCommentNotes; |
| private RobotCommentNotes robotCommentNotes; |
| |
| // Lazy defensive copies of mutable ReviewDb types, to avoid polluting the |
| // ChangeNotesCache from handlers. |
| private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets; |
| private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals; |
| private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvalsWithCopied; |
| private ImmutableSet<Comment.Key> commentKeys; |
| |
| public ChangeNotes( |
| Args args, |
| Change change, |
| boolean shouldExist, |
| @Nullable RefCache refs, |
| @Nullable ObjectId metaSha1) { |
| super(args, change.getId(), metaSha1); |
| this.change = new Change(change); |
| this.shouldExist = shouldExist; |
| this.refs = refs; |
| } |
| |
| @VisibleForTesting |
| public ChangeNotes(Args args, Change change, boolean shouldExist, @Nullable RefCache refs) { |
| this(args, change, shouldExist, refs, null); |
| } |
| |
| public Change getChange() { |
| return change; |
| } |
| |
| public ObjectId getMetaId() { |
| return state.metaId(); |
| } |
| |
| public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() { |
| if (patchSets == null) { |
| ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b = |
| ImmutableSortedMap.orderedBy(comparing(PatchSet.Id::get)); |
| b.putAll(state.patchSets()); |
| patchSets = b.build(); |
| } |
| return patchSets; |
| } |
| |
| /** |
| * Gets the approvals, not including the copied approvals. To get copied approvals as well, use |
| * {@link #getApprovalsWithCopied}, or use {@code ApprovalInference}. |
| */ |
| public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() { |
| if (approvals == null) { |
| approvals = |
| state.approvals().stream() |
| .filter(e -> !e.getValue().copied()) |
| .collect(toImmutableListMultimap(e -> e.getKey(), e -> e.getValue())); |
| } |
| return approvals; |
| } |
| |
| /** |
| * This method is currently used only in tests. TODO(paiking): Use this method to fetch approvals |
| * (including copied approvals) instead of computing copied approvals on demand. This will be used |
| * by {@code ApprovalCache}. |
| * |
| * @return all approvals, including copied approvals. |
| */ |
| public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovalsWithCopied() { |
| if (approvalsWithCopied == null) { |
| approvalsWithCopied = ImmutableListMultimap.copyOf(state.approvals()); |
| } |
| return approvalsWithCopied; |
| } |
| |
| public ReviewerSet getReviewers() { |
| return state.reviewers(); |
| } |
| |
| /** Returns reviewers that do not currently have a Gerrit account and were added by email. */ |
| public ReviewerByEmailSet getReviewersByEmail() { |
| return state.reviewersByEmail(); |
| } |
| |
| /** Returns reviewers that were modified during this change's current WIP phase. */ |
| public ReviewerSet getPendingReviewers() { |
| return state.pendingReviewers(); |
| } |
| |
| /** Returns reviewers by email that were modified during this change's current WIP phase. */ |
| public ReviewerByEmailSet getPendingReviewersByEmail() { |
| return state.pendingReviewersByEmail(); |
| } |
| |
| public ImmutableList<ReviewerStatusUpdate> getReviewerUpdates() { |
| return state.reviewerUpdates(); |
| } |
| |
| /** Returns the most recent update (i.e. status) per user. */ |
| public ImmutableSet<AttentionSetUpdate> getAttentionSet() { |
| return state.attentionSet(); |
| } |
| |
| /** Returns all updates for the attention set. */ |
| public ImmutableList<AttentionSetUpdate> getAttentionSetUpdates() { |
| return state.allAttentionSetUpdates(); |
| } |
| |
| /** |
| * Returns the evaluated submit requirements for the change. We only intend to store submit |
| * requirements in NoteDb for closed changes, hence the result will be an empty list for active |
| * changes, or a list of submit requirements results otherwise. For closed changes, the results |
| * represent the state of evaluating submit requirements for this change when it was merged. |
| */ |
| public ImmutableList<SubmitRequirementResult> getSubmitRequirementsResult() { |
| return state.submitRequirementsResult(); |
| } |
| |
| /** |
| * Returns an ImmutableSet of Account.Ids of all users that have been assigned to this change. The |
| * order of the set is the order in which they were assigned. |
| */ |
| public ImmutableSet<Account.Id> getPastAssignees() { |
| return Lists.reverse(state.assigneeUpdates()).stream() |
| .map(AssigneeStatusUpdate::currentAssignee) |
| .filter(Optional::isPresent) |
| .map(Optional::get) |
| .collect(toImmutableSet()); |
| } |
| |
| /** |
| * Returns an ImmutableList of AssigneeStatusUpdate of all the updates to the assignee field to |
| * this change. The order of the list is from most recent updates to least recent. |
| */ |
| public ImmutableList<AssigneeStatusUpdate> getAssigneeUpdates() { |
| return state.assigneeUpdates(); |
| } |
| |
| /** Returns an ImmutableSet of all hashtags for this change sorted in alphabetical order. */ |
| public ImmutableSet<String> getHashtags() { |
| return ImmutableSortedSet.copyOf(state.hashtags()); |
| } |
| |
| /** Returns a list of all users who have ever been a reviewer on this change. */ |
| public ImmutableList<Account.Id> getAllPastReviewers() { |
| return state.allPastReviewers(); |
| } |
| |
| /** |
| * Returns submit records stored during the most recent submit; only for changes that were |
| * actually submitted. |
| */ |
| public ImmutableList<SubmitRecord> getSubmitRecords() { |
| return state.submitRecords(); |
| } |
| |
| /** Returns all change messages, in chronological order, oldest first. */ |
| public ImmutableList<ChangeMessage> getChangeMessages() { |
| return state.changeMessages(); |
| } |
| |
| /** Returns inline comments on each revision. */ |
| public ImmutableListMultimap<ObjectId, HumanComment> getHumanComments() { |
| return state.publishedComments(); |
| } |
| |
| public ImmutableSet<Comment.Key> getCommentKeys() { |
| if (commentKeys == null) { |
| ImmutableSet.Builder<Comment.Key> b = ImmutableSet.builder(); |
| for (Comment c : getHumanComments().values()) { |
| b.add(new Comment.Key(c.key)); |
| } |
| commentKeys = b.build(); |
| } |
| return commentKeys; |
| } |
| |
| public int getUpdateCount() { |
| return state.updateCount(); |
| } |
| |
| /** Returns {@link Optional} value of time when the change was merged. */ |
| public Optional<Timestamp> getMergedOn() { |
| return Optional.ofNullable(state.mergedOn()); |
| } |
| |
| public ImmutableListMultimap<ObjectId, HumanComment> getDraftComments(Account.Id author) { |
| return getDraftComments(author, null); |
| } |
| |
| public ImmutableListMultimap<ObjectId, HumanComment> getDraftComments( |
| Account.Id author, @Nullable Ref ref) { |
| loadDraftComments(author, ref); |
| // Filter out any zombie draft comments. These are drafts that are also in |
| // the published map, and arise when the update to All-Users to delete them |
| // during the publish operation failed. |
| return ImmutableListMultimap.copyOf( |
| Multimaps.filterEntries( |
| draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key))); |
| } |
| |
| public ImmutableListMultimap<ObjectId, RobotComment> getRobotComments() { |
| loadRobotComments(); |
| return robotCommentNotes.getComments(); |
| } |
| |
| /** |
| * If draft comments have already been loaded for this author, then they will not be reloaded. |
| * However, this method will load the comments if no draft comments have been loaded or if the |
| * caller would like the drafts for another author. |
| */ |
| private void loadDraftComments(Account.Id author, @Nullable Ref ref) { |
| if (draftCommentNotes == null || !author.equals(draftCommentNotes.getAuthor()) || ref != null) { |
| draftCommentNotes = new DraftCommentNotes(args, getChangeId(), author, ref); |
| draftCommentNotes.load(); |
| } |
| } |
| |
| private void loadRobotComments() { |
| if (robotCommentNotes == null) { |
| robotCommentNotes = new RobotCommentNotes(args, change); |
| robotCommentNotes.load(); |
| } |
| } |
| |
| @VisibleForTesting |
| DraftCommentNotes getDraftCommentNotes() { |
| return draftCommentNotes; |
| } |
| |
| public RobotCommentNotes getRobotCommentNotes() { |
| loadRobotComments(); |
| return robotCommentNotes; |
| } |
| |
| public boolean containsComment(HumanComment c) { |
| if (containsCommentPublished(c)) { |
| return true; |
| } |
| loadDraftComments(c.author.getId(), null); |
| return draftCommentNotes.containsComment(c); |
| } |
| |
| public boolean containsCommentPublished(Comment c) { |
| for (Comment l : getHumanComments().values()) { |
| if (c.key.equals(l.key)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public String getRefName() { |
| return changeMetaRef(getChangeId()); |
| } |
| |
| public PatchSet getCurrentPatchSet() { |
| PatchSet.Id psId = change.currentPatchSetId(); |
| if (psId == null || getPatchSets().get(psId) == null) { |
| // In some cases, the current patch-set doesn't exist yet as it's being created during the |
| // operation (e.g rebase). |
| PatchSet currentPatchset = |
| getPatchSets().values().stream() |
| .max((p1, p2) -> p1.id().get() - p2.id().get()) |
| .orElseThrow( |
| () -> |
| new IllegalStateException( |
| String.format( |
| "change %s can't load any patchset", getChangeId().toString()))); |
| return currentPatchset; |
| } |
| return getPatchSets().get(psId); |
| } |
| |
| @Override |
| protected void onLoad(LoadHandle handle) throws NoSuchChangeException, IOException { |
| ObjectId rev = handle.id(); |
| if (rev == null) { |
| if (shouldExist) { |
| throw new NoSuchChangeException(getChangeId()); |
| } |
| loadDefaults(); |
| return; |
| } |
| |
| ChangeNotesCache.Value v = |
| args.cache.get().get(getProjectName(), getChangeId(), rev, handle::walk); |
| state = v.state(); |
| |
| String stateServerId = state.serverId(); |
| /** |
| * In earlier Gerrit versions serverId wasn't part of the change notes cache. That's why the |
| * earlier cached entries don't have the serverId attribute. That's fine because in earlier |
| * gerrit version serverId was already validated. Another approach to simplify the check would |
| * be to bump the cache version, but that would invalidate all persistent cache entries, what we |
| * rather try to avoid. |
| */ |
| if (!Strings.isNullOrEmpty(stateServerId) && !args.serverId.equals(stateServerId)) { |
| throw new InvalidServerIdException(args.serverId, stateServerId); |
| } |
| |
| state.copyColumnsTo(change); |
| revisionNoteMap = v.revisionNoteMap(); |
| } |
| |
| @Override |
| protected void loadDefaults() { |
| state = ChangeNotesState.empty(change); |
| } |
| |
| @Override |
| public Project.NameKey getProjectName() { |
| return change.getProject(); |
| } |
| |
| @Override |
| protected ObjectId readRef(Repository repo) throws IOException { |
| return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo); |
| } |
| } |