LCOV - code coverage report
Current view: top level - server/query/change - ChangeData.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 474 572 82.9 %
Date: 2022-11-19 15:00:39 Functions: 105 116 90.5 %

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

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