LCOV - code coverage report
Current view: top level - server/notedb - ChangeNotes.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 163 211 77.3 %
Date: 2022-11-19 15:00:39 Functions: 63 75 84.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750