LCOV - code coverage report
Current view: top level - server/change - ChangeJson.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 401 449 89.3 %
Date: 2022-11-19 15:00:39 Functions: 55 56 98.2 %

          Line data    Source code
       1             : // Copyright (C) 2012 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.change;
      16             : 
      17             : import static com.google.common.collect.ImmutableMap.toImmutableMap;
      18             : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
      19             : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
      20             : import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
      21             : import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
      22             : import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
      23             : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
      24             : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
      25             : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
      26             : import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
      27             : import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
      28             : import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
      29             : import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
      30             : import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
      31             : import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
      32             : import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_DIFFSTAT;
      33             : import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
      34             : import static com.google.gerrit.extensions.client.ListChangesOption.SUBMIT_REQUIREMENTS;
      35             : import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
      36             : import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
      37             : import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
      38             : import static com.google.gerrit.server.util.AttentionSetUtil.removalsOnly;
      39             : import static java.util.stream.Collectors.toList;
      40             : 
      41             : import com.google.common.base.Joiner;
      42             : import com.google.common.base.MoreObjects;
      43             : import com.google.common.base.Throwables;
      44             : import com.google.common.collect.ImmutableList;
      45             : import com.google.common.collect.ImmutableListMultimap;
      46             : import com.google.common.collect.ImmutableMap;
      47             : import com.google.common.collect.ImmutableSet;
      48             : import com.google.common.collect.ImmutableSortedMap;
      49             : import com.google.common.collect.ListMultimap;
      50             : import com.google.common.collect.Lists;
      51             : import com.google.common.collect.Maps;
      52             : import com.google.common.collect.Sets;
      53             : import com.google.common.flogger.FluentLogger;
      54             : import com.google.gerrit.common.Nullable;
      55             : import com.google.gerrit.entities.Account;
      56             : import com.google.gerrit.entities.Address;
      57             : import com.google.gerrit.entities.Change;
      58             : import com.google.gerrit.entities.ChangeMessage;
      59             : import com.google.gerrit.entities.LegacySubmitRequirement;
      60             : import com.google.gerrit.entities.PatchSet;
      61             : import com.google.gerrit.entities.PatchSetApproval;
      62             : import com.google.gerrit.entities.Project;
      63             : import com.google.gerrit.entities.RefNames;
      64             : import com.google.gerrit.entities.SubmitRecord;
      65             : import com.google.gerrit.entities.SubmitRecord.Status;
      66             : import com.google.gerrit.entities.SubmitRequirementResult;
      67             : import com.google.gerrit.entities.SubmitTypeRecord;
      68             : import com.google.gerrit.exceptions.StorageException;
      69             : import com.google.gerrit.extensions.api.changes.FixInput;
      70             : import com.google.gerrit.extensions.client.ListChangesOption;
      71             : import com.google.gerrit.extensions.client.ReviewerState;
      72             : import com.google.gerrit.extensions.common.AccountInfo;
      73             : import com.google.gerrit.extensions.common.ApprovalInfo;
      74             : import com.google.gerrit.extensions.common.ChangeInfo;
      75             : import com.google.gerrit.extensions.common.ChangeMessageInfo;
      76             : import com.google.gerrit.extensions.common.LabelInfo;
      77             : import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
      78             : import com.google.gerrit.extensions.common.PluginDefinedInfo;
      79             : import com.google.gerrit.extensions.common.ProblemInfo;
      80             : import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
      81             : import com.google.gerrit.extensions.common.RevisionInfo;
      82             : import com.google.gerrit.extensions.common.SubmitRecordInfo;
      83             : import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
      84             : import com.google.gerrit.extensions.common.TrackingIdInfo;
      85             : import com.google.gerrit.extensions.restapi.Url;
      86             : import com.google.gerrit.index.RefState;
      87             : import com.google.gerrit.index.query.QueryResult;
      88             : import com.google.gerrit.metrics.Description;
      89             : import com.google.gerrit.metrics.Description.Units;
      90             : import com.google.gerrit.metrics.MetricMaker;
      91             : import com.google.gerrit.metrics.Timer0;
      92             : import com.google.gerrit.server.ChangeMessagesUtil;
      93             : import com.google.gerrit.server.CurrentUser;
      94             : import com.google.gerrit.server.GpgException;
      95             : import com.google.gerrit.server.ReviewerByEmailSet;
      96             : import com.google.gerrit.server.ReviewerSet;
      97             : import com.google.gerrit.server.ReviewerStatusUpdate;
      98             : import com.google.gerrit.server.StarredChangesUtil;
      99             : import com.google.gerrit.server.account.AccountInfoComparator;
     100             : import com.google.gerrit.server.account.AccountLoader;
     101             : import com.google.gerrit.server.cancellation.RequestCancelledException;
     102             : import com.google.gerrit.server.config.GerritServerConfig;
     103             : import com.google.gerrit.server.config.TrackingFooters;
     104             : import com.google.gerrit.server.index.change.ChangeField;
     105             : import com.google.gerrit.server.notedb.ChangeNotes;
     106             : import com.google.gerrit.server.notedb.ReviewerStateInternal;
     107             : import com.google.gerrit.server.patch.PatchListNotAvailableException;
     108             : import com.google.gerrit.server.permissions.ChangePermission;
     109             : import com.google.gerrit.server.permissions.PermissionBackend;
     110             : import com.google.gerrit.server.permissions.PermissionBackendException;
     111             : import com.google.gerrit.server.project.RemoveReviewerControl;
     112             : import com.google.gerrit.server.project.SubmitRuleOptions;
     113             : import com.google.gerrit.server.query.change.ChangeData;
     114             : import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
     115             : import com.google.gerrit.server.util.AttentionSetUtil;
     116             : import com.google.inject.Inject;
     117             : import com.google.inject.Provider;
     118             : import com.google.inject.Singleton;
     119             : import com.google.inject.assistedinject.Assisted;
     120             : import java.io.IOException;
     121             : import java.util.ArrayList;
     122             : import java.util.Collection;
     123             : import java.util.Collections;
     124             : import java.util.HashMap;
     125             : import java.util.HashSet;
     126             : import java.util.List;
     127             : import java.util.Map;
     128             : import java.util.Optional;
     129             : import java.util.Set;
     130             : import java.util.stream.Collectors;
     131             : import org.eclipse.jgit.lib.Config;
     132             : import org.eclipse.jgit.lib.ObjectId;
     133             : 
     134             : /**
     135             :  * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
     136             :  *
     137             :  * <p>This is intended to be used on request scope, but may be used for converting multiple {@link
     138             :  * ChangeData} objects from different sources.
     139             :  */
     140             : public class ChangeJson {
     141         103 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     142             : 
     143         103 :   public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
     144         103 :       ChangeField.SUBMIT_RULE_OPTIONS_LENIENT.toBuilder().build();
     145             : 
     146         103 :   public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
     147         103 :       ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build();
     148             : 
     149         103 :   static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
     150         103 :       ImmutableSet.of(
     151             :           ALL_COMMITS,
     152             :           ALL_REVISIONS,
     153             :           CHANGE_ACTIONS,
     154             :           CHECK,
     155             :           COMMIT_FOOTERS,
     156             :           CURRENT_ACTIONS,
     157             :           CURRENT_COMMIT,
     158             :           MESSAGES);
     159             : 
     160             :   @Singleton
     161             :   public static class Factory {
     162             :     private final AssistedFactory factory;
     163             : 
     164             :     @Inject
     165         151 :     Factory(AssistedFactory factory) {
     166         151 :       this.factory = factory;
     167         151 :     }
     168             : 
     169             :     public ChangeJson noOptions() {
     170          60 :       return create(ImmutableSet.of());
     171             :     }
     172             : 
     173             :     public ChangeJson create(Iterable<ListChangesOption> options) {
     174         103 :       return factory.create(options, Optional.empty());
     175             :     }
     176             : 
     177             :     public ChangeJson create(
     178             :         Iterable<ListChangesOption> options, PluginDefinedInfosFactory pluginDefinedInfosFactory) {
     179          70 :       return factory.create(options, Optional.of(pluginDefinedInfosFactory));
     180             :     }
     181             : 
     182             :     public ChangeJson create(ListChangesOption first, ListChangesOption... rest) {
     183           5 :       return create(Sets.immutableEnumSet(first, rest));
     184             :     }
     185             :   }
     186             : 
     187             :   public interface AssistedFactory {
     188             :     ChangeJson create(
     189             :         Iterable<ListChangesOption> options,
     190             :         Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory);
     191             :   }
     192             : 
     193             :   @Singleton
     194             :   private static class Metrics {
     195             :     private final Timer0 toChangeInfoLatency;
     196             :     private final Timer0 toChangeInfosLatency;
     197             :     private final Timer0 formatQueryResultsLatency;
     198             : 
     199             :     @Inject
     200         103 :     Metrics(MetricMaker metricMaker) {
     201         103 :       toChangeInfoLatency =
     202         103 :           metricMaker.newTimer(
     203             :               "http/server/rest_api/change_json/to_change_info_latency",
     204             :               new Description("Latency for toChangeInfo invocations in ChangeJson")
     205         103 :                   .setCumulative()
     206         103 :                   .setUnit(Units.MILLISECONDS));
     207         103 :       toChangeInfosLatency =
     208         103 :           metricMaker.newTimer(
     209             :               "http/server/rest_api/change_json/to_change_infos_latency",
     210             :               new Description("Latency for toChangeInfos invocations in ChangeJson")
     211         103 :                   .setCumulative()
     212         103 :                   .setUnit(Units.MILLISECONDS));
     213         103 :       formatQueryResultsLatency =
     214         103 :           metricMaker.newTimer(
     215             :               "http/server/rest_api/change_json/format_query_results_latency",
     216             :               new Description("Latency for formatQueryResults invocations in ChangeJson")
     217         103 :                   .setCumulative()
     218         103 :                   .setUnit(Units.MILLISECONDS));
     219         103 :     }
     220             :   }
     221             : 
     222             :   private final Provider<CurrentUser> userProvider;
     223             :   private final PermissionBackend permissionBackend;
     224             :   private final ChangeData.Factory changeDataFactory;
     225             :   private final AccountLoader.Factory accountLoaderFactory;
     226             :   private final ImmutableSet<ListChangesOption> options;
     227             :   private final ChangeMessagesUtil cmUtil;
     228             :   private final Provider<ConsistencyChecker> checkerProvider;
     229             :   private final ActionJson actionJson;
     230             :   private final ChangeNotes.Factory notesFactory;
     231             :   private final LabelsJson labelsJson;
     232             :   private final RemoveReviewerControl removeReviewerControl;
     233             :   private final TrackingFooters trackingFooters;
     234             :   private final Metrics metrics;
     235             :   private final RevisionJson revisionJson;
     236             :   private final Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory;
     237             :   private final boolean includeMergeable;
     238             :   private final boolean lazyLoad;
     239             :   private final boolean cacheQueryResultsByChangeNum;
     240             : 
     241             :   private AccountLoader accountLoader;
     242             :   private FixInput fix;
     243             : 
     244             :   @Inject
     245             :   ChangeJson(
     246             :       Provider<CurrentUser> user,
     247             :       PermissionBackend permissionBackend,
     248             :       ChangeData.Factory cdf,
     249             :       AccountLoader.Factory ailf,
     250             :       ChangeMessagesUtil cmUtil,
     251             :       Provider<ConsistencyChecker> checkerProvider,
     252             :       ActionJson actionJson,
     253             :       ChangeNotes.Factory notesFactory,
     254             :       LabelsJson labelsJson,
     255             :       RemoveReviewerControl removeReviewerControl,
     256             :       TrackingFooters trackingFooters,
     257             :       Metrics metrics,
     258             :       RevisionJson.Factory revisionJsonFactory,
     259             :       @GerritServerConfig Config cfg,
     260             :       @Assisted Iterable<ListChangesOption> options,
     261         103 :       @Assisted Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory) {
     262         103 :     this.userProvider = user;
     263         103 :     this.changeDataFactory = cdf;
     264         103 :     this.permissionBackend = permissionBackend;
     265         103 :     this.accountLoaderFactory = ailf;
     266         103 :     this.cmUtil = cmUtil;
     267         103 :     this.checkerProvider = checkerProvider;
     268         103 :     this.actionJson = actionJson;
     269         103 :     this.notesFactory = notesFactory;
     270         103 :     this.labelsJson = labelsJson;
     271         103 :     this.removeReviewerControl = removeReviewerControl;
     272         103 :     this.trackingFooters = trackingFooters;
     273         103 :     this.metrics = metrics;
     274         103 :     this.revisionJson = revisionJsonFactory.create(options);
     275         103 :     this.options = Sets.immutableEnumSet(options);
     276         103 :     this.includeMergeable = MergeabilityComputationBehavior.fromConfig(cfg).includeInApi();
     277         103 :     this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD);
     278         103 :     this.pluginDefinedInfosFactory = pluginDefinedInfosFactory;
     279         103 :     this.cacheQueryResultsByChangeNum =
     280         103 :         cfg.getBoolean("index", "cacheQueryResultsByChangeNum", true);
     281             : 
     282         103 :     logger.atFine().log("options = %s", options);
     283         103 :   }
     284             : 
     285             :   public ChangeJson fix(FixInput fix) {
     286           3 :     this.fix = fix;
     287           3 :     return this;
     288             :   }
     289             : 
     290             :   public ChangeInfo format(ChangeResource rsrc) {
     291           2 :     return format(changeDataFactory.create(rsrc.getNotes()));
     292             :   }
     293             : 
     294             :   public ChangeInfo format(Change change) {
     295          60 :     return format(changeDataFactory.create(change));
     296             :   }
     297             : 
     298             :   public ChangeInfo format(Change change, @Nullable ObjectId metaRevId) {
     299          67 :     ChangeNotes notes = notesFactory.createChecked(change.getProject(), change.getId(), metaRevId);
     300          67 :     return format(changeDataFactory.create(notes));
     301             :   }
     302             : 
     303             :   public ChangeInfo format(ChangeData cd) {
     304         103 :     return format(cd, Optional.empty(), true, getPluginInfos(cd));
     305             :   }
     306             : 
     307             :   public ChangeInfo format(RevisionResource rsrc) {
     308           3 :     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     309           3 :     return format(cd, Optional.of(rsrc.getPatchSet().id()), true, getPluginInfos(cd));
     310             :   }
     311             : 
     312             :   public List<List<ChangeInfo>> format(List<QueryResult<ChangeData>> in)
     313             :       throws PermissionBackendException {
     314          26 :     try (Timer0.Context ignored = metrics.formatQueryResultsLatency.start()) {
     315          26 :       accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     316          26 :       List<List<ChangeInfo>> res = new ArrayList<>(in.size());
     317          26 :       Map<Change.Id, ChangeInfo> cache = Maps.newHashMapWithExpectedSize(in.size());
     318          26 :       ImmutableListMultimap<Change.Id, PluginDefinedInfo> pluginInfosByChange =
     319          26 :           getPluginInfos(in.stream().flatMap(e -> e.entities().stream()).collect(toList()));
     320          26 :       for (QueryResult<ChangeData> r : in) {
     321          26 :         List<ChangeInfo> infos = toChangeInfos(r.entities(), cache, pluginInfosByChange);
     322          26 :         if (!infos.isEmpty() && r.more()) {
     323           6 :           infos.get(infos.size() - 1)._moreChanges = true;
     324             :         }
     325          26 :         res.add(infos);
     326          26 :       }
     327          26 :       accountLoader.fill();
     328          26 :       return res;
     329             :     }
     330             :   }
     331             : 
     332             :   public List<ChangeInfo> format(Collection<ChangeData> in) throws PermissionBackendException {
     333          11 :     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     334          11 :     ensureLoaded(in);
     335          11 :     List<ChangeInfo> out = new ArrayList<>(in.size());
     336          11 :     ImmutableListMultimap<Change.Id, PluginDefinedInfo> pluginInfosByChange = getPluginInfos(in);
     337          11 :     for (ChangeData cd : in) {
     338          10 :       out.add(format(cd, Optional.empty(), false, pluginInfosByChange.get(cd.getId())));
     339          10 :     }
     340          11 :     accountLoader.fill();
     341          11 :     return out;
     342             :   }
     343             : 
     344             :   public ChangeInfo format(Project.NameKey project, Change.Id id) {
     345          25 :     return format(project, id, null);
     346             :   }
     347             : 
     348             :   public ChangeInfo format(Project.NameKey project, Change.Id id, @Nullable ObjectId metaRevId) {
     349             :     ChangeNotes notes;
     350             :     try {
     351          25 :       notes = notesFactory.createChecked(project, id, metaRevId);
     352           0 :     } catch (StorageException e) {
     353           0 :       if (!has(CHECK)) {
     354           0 :         throw e;
     355             :       }
     356           0 :       return checkOnly(changeDataFactory.create(project, id));
     357          25 :     }
     358          25 :     ChangeData cd = changeDataFactory.create(notes);
     359          25 :     return format(cd, Optional.empty(), true, getPluginInfos(cd));
     360             :   }
     361             : 
     362             :   private static Collection<LegacySubmitRequirementInfo> requirementsFor(ChangeData cd) {
     363         103 :     Collection<LegacySubmitRequirementInfo> reqInfos = new ArrayList<>();
     364         103 :     for (SubmitRecord submitRecord : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
     365         103 :       if (submitRecord.requirements == null) {
     366         103 :         continue;
     367             :       }
     368           3 :       for (LegacySubmitRequirement requirement : submitRecord.requirements) {
     369           3 :         reqInfos.add(requirementToInfo(requirement, submitRecord.status));
     370           3 :       }
     371           3 :     }
     372         103 :     return reqInfos;
     373             :   }
     374             : 
     375             :   private Collection<SubmitRecordInfo> submitRecordsFor(ChangeData cd) {
     376         103 :     List<SubmitRecordInfo> submitRecordInfos = new ArrayList<>();
     377         103 :     for (SubmitRecord record : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
     378         103 :       submitRecordInfos.add(submitRecordToInfo(record));
     379         103 :     }
     380         103 :     return submitRecordInfos;
     381             :   }
     382             : 
     383             :   private Collection<SubmitRequirementResultInfo> submitRequirementsFor(ChangeData cd) {
     384         103 :     Collection<SubmitRequirementResultInfo> reqInfos = new ArrayList<>();
     385         103 :     cd.submitRequirementsIncludingLegacy().entrySet().stream()
     386         103 :         .filter(entry -> !entry.getValue().isHidden())
     387         103 :         .forEach(
     388         103 :             entry -> reqInfos.add(SubmitRequirementsJson.toInfo(entry.getKey(), entry.getValue())));
     389         103 :     return reqInfos;
     390             :   }
     391             : 
     392             :   private static LegacySubmitRequirementInfo requirementToInfo(
     393             :       LegacySubmitRequirement req, Status status) {
     394           3 :     return new LegacySubmitRequirementInfo(status.name(), req.fallbackText(), req.type());
     395             :   }
     396             : 
     397             :   private SubmitRecordInfo submitRecordToInfo(SubmitRecord record) {
     398         103 :     SubmitRecordInfo info = new SubmitRecordInfo();
     399         103 :     if (record.status != null) {
     400         103 :       info.status = SubmitRecordInfo.Status.valueOf(record.status.name());
     401             :     }
     402         103 :     info.ruleName = record.ruleName;
     403         103 :     info.errorMessage = record.errorMessage;
     404         103 :     if (record.labels != null) {
     405         103 :       info.labels = new ArrayList<>();
     406         103 :       for (SubmitRecord.Label label : record.labels) {
     407         103 :         SubmitRecordInfo.Label labelInfo = new SubmitRecordInfo.Label();
     408         103 :         labelInfo.label = label.label;
     409         103 :         if (label.status != null) {
     410         103 :           labelInfo.status = SubmitRecordInfo.Label.Status.valueOf(label.status.name());
     411             :         }
     412         103 :         labelInfo.appliedBy = accountLoader.get(label.appliedBy);
     413         103 :         info.labels.add(labelInfo);
     414         103 :       }
     415             :     }
     416         103 :     if (record.requirements != null) {
     417           3 :       info.requirements = new ArrayList<>();
     418           3 :       for (LegacySubmitRequirement requirement : record.requirements) {
     419           3 :         info.requirements.add(requirementToInfo(requirement, record.status));
     420           3 :       }
     421             :     }
     422         103 :     return info;
     423             :   }
     424             : 
     425             :   private static void finish(ChangeInfo info) {
     426         103 :     info.id =
     427         103 :         Joiner.on('~')
     428         103 :             .join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId));
     429         103 :   }
     430             : 
     431             :   private static boolean containsAnyOf(
     432             :       ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
     433         103 :     return !Sets.intersection(toFind, set).isEmpty();
     434             :   }
     435             : 
     436             :   private ChangeInfo format(
     437             :       ChangeData cd,
     438             :       Optional<PatchSet.Id> limitToPsId,
     439             :       boolean fillAccountLoader,
     440             :       List<PluginDefinedInfo> pluginInfosForChange) {
     441             :     try {
     442         103 :       if (fillAccountLoader) {
     443         103 :         accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     444         103 :         ChangeInfo res = toChangeInfo(cd, limitToPsId, pluginInfosForChange);
     445         103 :         accountLoader.fill();
     446         103 :         return res;
     447             :       }
     448          32 :       return toChangeInfo(cd, limitToPsId, pluginInfosForChange);
     449           1 :     } catch (PatchListNotAvailableException
     450             :         | GpgException
     451             :         | IOException
     452             :         | PermissionBackendException
     453             :         | RuntimeException e) {
     454           1 :       if (!has(CHECK)) {
     455           1 :         Throwables.throwIfInstanceOf(e, StorageException.class);
     456           1 :         throw new StorageException(e);
     457             :       }
     458           0 :       return checkOnly(cd);
     459             :     }
     460             :   }
     461             : 
     462             :   private void ensureLoaded(Iterable<ChangeData> all) {
     463          33 :     if (lazyLoad) {
     464           3 :       for (ChangeData cd : all) {
     465             :         // Mark all ChangeDatas as coming from the index, but allow backfilling data from NoteDb
     466           3 :         cd.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
     467           3 :       }
     468           3 :       ChangeData.ensureChangeLoaded(all);
     469           3 :       if (has(ALL_REVISIONS)) {
     470           3 :         ChangeData.ensureAllPatchSetsLoaded(all);
     471           1 :       } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
     472           1 :         ChangeData.ensureCurrentPatchSetLoaded(all);
     473             :       }
     474           3 :       if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
     475           0 :         ChangeData.ensureReviewedByLoadedForOpenChanges(all);
     476             :       }
     477           3 :       ChangeData.ensureCurrentApprovalsLoaded(all);
     478             :     } else {
     479          33 :       for (ChangeData cd : all) {
     480             :         // Mark all ChangeDatas as coming from the index. Disallow using NoteDb
     481          32 :         cd.setStorageConstraint(ChangeData.StorageConstraint.INDEX_ONLY);
     482          32 :       }
     483             :     }
     484          33 :   }
     485             : 
     486             :   private boolean has(ListChangesOption option) {
     487         103 :     return options.contains(option);
     488             :   }
     489             : 
     490             :   private List<ChangeInfo> toChangeInfos(
     491             :       List<ChangeData> changes,
     492             :       Map<Change.Id, ChangeInfo> cache,
     493             :       ImmutableListMultimap<Change.Id, PluginDefinedInfo> pluginInfosByChange) {
     494          26 :     try (Timer0.Context ignored = metrics.toChangeInfosLatency.start()) {
     495          26 :       List<ChangeInfo> changeInfos = new ArrayList<>(changes.size());
     496          26 :       for (int i = 0; i < changes.size(); i++) {
     497             :         // We can only cache and re-use an entity if it's not the last in the list. The last entity
     498             :         // may later get _moreChanges set. If it was cached or re-used, that setting would propagate
     499             :         // to the original entity yielding wrong results.
     500             :         // This problem has two sides where 'last in the list' has to be respected:
     501             :         // (1) Caching
     502             :         // (2) Reusing
     503          25 :         boolean isCacheable = cacheQueryResultsByChangeNum && (i != changes.size() - 1);
     504          25 :         ChangeData cd = changes.get(i);
     505          25 :         ChangeInfo info = cache.get(cd.getId());
     506          25 :         if (info != null && isCacheable) {
     507           1 :           changeInfos.add(info);
     508           1 :           continue;
     509             :         }
     510             : 
     511             :         // Compute and cache if possible
     512             :         try {
     513          25 :           ensureLoaded(Collections.singleton(cd));
     514          25 :           info = format(cd, Optional.empty(), false, pluginInfosByChange.get(cd.getId()));
     515          25 :           changeInfos.add(info);
     516          25 :           if (isCacheable) {
     517          16 :             cache.put(Change.id(info._number), info);
     518             :           }
     519           0 :         } catch (RuntimeException e) {
     520           0 :           Optional<RequestCancelledException> requestCancelledException =
     521           0 :               RequestCancelledException.getFromCausalChain(e);
     522           0 :           if (requestCancelledException.isPresent()) {
     523           0 :             throw e;
     524             :           }
     525           0 :           logger.atWarning().withCause(e).log(
     526           0 :               "Omitting corrupt change %s from results", cd.getId());
     527          25 :         }
     528             :       }
     529          26 :       return changeInfos;
     530             :     }
     531             :   }
     532             : 
     533             :   private ChangeInfo checkOnly(ChangeData cd) {
     534             :     ChangeNotes notes;
     535             :     try {
     536           0 :       notes = cd.notes();
     537           0 :     } catch (StorageException e) {
     538           0 :       String msg = "Error loading change";
     539           0 :       logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
     540           0 :       ChangeInfo info = new ChangeInfo();
     541           0 :       info._number = cd.getId().get();
     542           0 :       ProblemInfo p = new ProblemInfo();
     543           0 :       p.message = msg;
     544           0 :       info.problems = Lists.newArrayList(p);
     545           0 :       return info;
     546           0 :     }
     547             : 
     548           0 :     ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix);
     549           0 :     ChangeInfo info = new ChangeInfo();
     550           0 :     Change c = result.change();
     551           0 :     if (c != null) {
     552           0 :       info.project = c.getProject().get();
     553           0 :       info.branch = c.getDest().shortName();
     554           0 :       info.topic = c.getTopic();
     555           0 :       info.changeId = c.getKey().get();
     556           0 :       info.subject = c.getSubject();
     557           0 :       info.status = c.getStatus().asChangeStatus();
     558           0 :       info.owner = new AccountInfo(c.getOwner().get());
     559           0 :       info.setCreated(c.getCreatedOn());
     560           0 :       info.setUpdated(c.getLastUpdatedOn());
     561           0 :       info._number = c.getId().get();
     562           0 :       info.problems = result.problems();
     563           0 :       info.isPrivate = c.isPrivate() ? true : null;
     564           0 :       info.workInProgress = c.isWorkInProgress() ? true : null;
     565           0 :       info.hasReviewStarted = c.hasReviewStarted();
     566           0 :       finish(info);
     567             :     } else {
     568           0 :       info._number = result.id().get();
     569           0 :       info.problems = result.problems();
     570             :     }
     571           0 :     return info;
     572             :   }
     573             : 
     574             :   private ChangeInfo toChangeInfo(
     575             :       ChangeData cd,
     576             :       Optional<PatchSet.Id> limitToPsId,
     577             :       List<PluginDefinedInfo> pluginInfosForChange)
     578             :       throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
     579         103 :     try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
     580         103 :       return toChangeInfoImpl(cd, limitToPsId, pluginInfosForChange);
     581             :     }
     582             :   }
     583             : 
     584             :   private ChangeInfo toChangeInfoImpl(
     585             :       ChangeData cd, Optional<PatchSet.Id> limitToPsId, List<PluginDefinedInfo> pluginInfos)
     586             :       throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
     587         103 :     ChangeInfo out = new ChangeInfo();
     588         103 :     CurrentUser user = userProvider.get();
     589             : 
     590         103 :     if (has(CHECK)) {
     591           5 :       out.problems = checkerProvider.get().check(cd.notes(), fix).problems();
     592             :       // If any problems were fixed, the ChangeData needs to be reloaded.
     593           5 :       for (ProblemInfo p : out.problems) {
     594           2 :         if (p.status == ProblemInfo.Status.FIXED) {
     595           2 :           cd = changeDataFactory.create(cd.project(), cd.getId());
     596           2 :           break;
     597             :         }
     598           1 :       }
     599             :     }
     600             : 
     601         103 :     Change in = cd.change();
     602         103 :     out.project = in.getProject().get();
     603         103 :     out.branch = in.getDest().shortName();
     604         103 :     out.topic = in.getTopic();
     605         103 :     if (!cd.attentionSet().isEmpty()) {
     606          49 :       out.removedFromAttentionSet =
     607          49 :           removalsOnly(cd.attentionSet()).stream()
     608          49 :               .collect(
     609          49 :                   toImmutableMap(
     610          39 :                       a -> a.account().get(),
     611          39 :                       a -> AttentionSetUtil.createAttentionSetInfo(a, accountLoader)));
     612          49 :       out.attentionSet =
     613             :           // This filtering should match GetAttentionSet.
     614          49 :           additionsOnly(cd.attentionSet()).stream()
     615          49 :               .collect(
     616          49 :                   toImmutableMap(
     617          49 :                       a -> a.account().get(),
     618          49 :                       a -> AttentionSetUtil.createAttentionSetInfo(a, accountLoader)));
     619             :     }
     620         103 :     out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null;
     621         103 :     out.hashtags = cd.hashtags();
     622         103 :     out.changeId = in.getKey().get();
     623         103 :     if (in.isNew()) {
     624         103 :       SubmitTypeRecord str = cd.submitTypeRecord();
     625         103 :       if (str.isOk()) {
     626         103 :         out.submitType = str.type;
     627             :       }
     628         103 :       if (includeMergeable) {
     629          14 :         out.mergeable = cd.isMergeable();
     630             :       }
     631         103 :       if (has(SUBMITTABLE)) {
     632         103 :         out.submittable = submittable(cd);
     633             :       }
     634             :     }
     635         103 :     if (!has(SKIP_DIFFSTAT)) {
     636          81 :       Optional<ChangedLines> changedLines = cd.changedLines();
     637          81 :       if (changedLines.isPresent()) {
     638          81 :         out.insertions = changedLines.get().insertions;
     639          81 :         out.deletions = changedLines.get().deletions;
     640             :       }
     641             :     }
     642         103 :     out.isPrivate = in.isPrivate() ? true : null;
     643         103 :     out.workInProgress = in.isWorkInProgress() ? true : null;
     644         103 :     out.hasReviewStarted = in.hasReviewStarted();
     645         103 :     out.subject = in.getSubject();
     646         103 :     out.status = in.getStatus().asChangeStatus();
     647         103 :     out.owner = accountLoader.get(in.getOwner());
     648         103 :     out.setCreated(in.getCreatedOn());
     649         103 :     out.setUpdated(in.getLastUpdatedOn());
     650         103 :     out._number = in.getId().get();
     651         103 :     out.totalCommentCount = cd.totalCommentCount();
     652         103 :     out.unresolvedCommentCount = cd.unresolvedCommentCount();
     653             : 
     654         103 :     if (cd.getRefStates() != null) {
     655         103 :       String metaName = RefNames.changeMetaRef(cd.getId());
     656         103 :       Optional<RefState> metaState =
     657         103 :           cd.getRefStates().values().stream().filter(r -> r.ref().equals(metaName)).findAny();
     658             : 
     659             :       // metaState should always be there, but it doesn't hurt to be extra careful.
     660         103 :       metaState.ifPresent(rs -> out.metaRevId = rs.id().getName());
     661             :     }
     662             : 
     663         103 :     if (user.isIdentifiedUser()) {
     664         103 :       Collection<String> stars = cd.stars(user.getAccountId());
     665         103 :       out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
     666         103 :       if (!stars.isEmpty()) {
     667           6 :         out.stars = stars;
     668             :       }
     669             :     }
     670             : 
     671         103 :     if (in.isNew() && has(REVIEWED) && user.isIdentifiedUser()) {
     672         103 :       out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
     673             :     }
     674             : 
     675         103 :     out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS));
     676         103 :     out.requirements = requirementsFor(cd);
     677         103 :     out.submitRecords = submitRecordsFor(cd);
     678         103 :     if (has(SUBMIT_REQUIREMENTS)) {
     679         103 :       out.submitRequirements = submitRequirementsFor(cd);
     680             :     }
     681             : 
     682         103 :     if (out.labels != null && has(DETAILED_LABELS)) {
     683             :       // If limited to specific patch sets but not the current patch set, don't
     684             :       // list permitted labels, since users can't vote on those patch sets.
     685         103 :       if (user.isIdentifiedUser()
     686         103 :           && (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) {
     687         103 :         out.permittedLabels =
     688         103 :             !cd.change().isAbandoned()
     689         103 :                 ? labelsJson.permittedLabels(user.getAccountId(), cd)
     690         103 :                 : ImmutableMap.of();
     691             :       }
     692             :     }
     693             : 
     694         103 :     if (has(LABELS) || has(DETAILED_LABELS)) {
     695         103 :       out.reviewers = reviewerMap(cd.reviewers(), cd.reviewersByEmail(), false);
     696         103 :       out.pendingReviewers = reviewerMap(cd.pendingReviewers(), cd.pendingReviewersByEmail(), true);
     697         103 :       out.removableReviewers = removableReviewers(cd, out);
     698             :     }
     699             : 
     700         103 :     setSubmitter(cd, out);
     701             : 
     702         103 :     if (!pluginInfos.isEmpty()) {
     703           2 :       out.plugins = pluginInfos;
     704             :     }
     705         103 :     out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null;
     706         103 :     out.submissionId = cd.change().getSubmissionId();
     707         103 :     out.cherryPickOfChange =
     708         103 :         cd.change().getCherryPickOf() != null
     709          10 :             ? cd.change().getCherryPickOf().changeId().get()
     710         103 :             : null;
     711         103 :     out.cherryPickOfPatchSet =
     712         103 :         cd.change().getCherryPickOf() != null ? cd.change().getCherryPickOf().get() : null;
     713             : 
     714         103 :     if (has(REVIEWER_UPDATES)) {
     715         103 :       out.reviewerUpdates = reviewerUpdates(cd);
     716             :     }
     717             : 
     718         103 :     boolean needMessages = has(MESSAGES);
     719         103 :     boolean needRevisions = has(ALL_REVISIONS) || has(CURRENT_REVISION) || limitToPsId.isPresent();
     720             :     Map<PatchSet.Id, PatchSet> src;
     721         103 :     if (needMessages || needRevisions) {
     722         103 :       src = loadPatchSets(cd, limitToPsId);
     723             :     } else {
     724          69 :       src = null;
     725             :     }
     726             : 
     727         103 :     if (needMessages) {
     728         103 :       out.messages = messages(cd);
     729             :     }
     730         103 :     finish(out);
     731             : 
     732             :     // This block must come after the ChangeInfo is mostly populated, since
     733             :     // it will be passed to ActionVisitors as-is.
     734         103 :     if (needRevisions) {
     735         103 :       out.revisions = revisionJson.getRevisions(accountLoader, cd, src, limitToPsId, out);
     736         103 :       if (out.revisions != null) {
     737         103 :         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
     738         103 :           if (entry.getValue().isCurrent) {
     739         103 :             out.currentRevision = entry.getKey();
     740         103 :             break;
     741             :           }
     742          52 :         }
     743             :       }
     744             :     }
     745             : 
     746         103 :     if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) {
     747          57 :       actionJson.addChangeActions(out, cd);
     748             :     }
     749             : 
     750         103 :     if (has(TRACKING_IDS)) {
     751         103 :       ListMultimap<String, String> set = trackingFooters.extract(cd.commitFooters());
     752         103 :       out.trackingIds =
     753         103 :           set.entries().stream()
     754         103 :               .map(e -> new TrackingIdInfo(e.getKey(), e.getValue()))
     755         103 :               .collect(toList());
     756             :     }
     757             : 
     758         103 :     return out;
     759             :   }
     760             : 
     761             :   private Map<ReviewerState, Collection<AccountInfo>> reviewerMap(
     762             :       ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, boolean includeRemoved) {
     763         103 :     Map<ReviewerState, Collection<AccountInfo>> reviewerMap = new HashMap<>();
     764         103 :     for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
     765         103 :       if (!includeRemoved && state == ReviewerStateInternal.REMOVED) {
     766         103 :         continue;
     767             :       }
     768         103 :       Collection<AccountInfo> reviewersByState = toAccountInfo(reviewers.byState(state));
     769         103 :       reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state)));
     770         103 :       if (!reviewersByState.isEmpty()) {
     771          72 :         reviewerMap.put(state.asReviewerState(), reviewersByState);
     772             :       }
     773             :     }
     774         103 :     return reviewerMap;
     775             :   }
     776             : 
     777             :   private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) {
     778         103 :     List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
     779         103 :     List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
     780         103 :     for (ReviewerStatusUpdate c : reviewerUpdates) {
     781          46 :       ReviewerUpdateInfo change =
     782             :           new ReviewerUpdateInfo(
     783          46 :               c.date(),
     784          46 :               accountLoader.get(c.updatedBy()),
     785          46 :               accountLoader.get(c.reviewer()),
     786          46 :               c.state().asReviewerState());
     787          46 :       result.add(change);
     788          46 :     }
     789         103 :     return result;
     790             :   }
     791             : 
     792             :   private boolean submittable(ChangeData cd) {
     793         103 :     return cd.submitRequirementsIncludingLegacy().values().stream()
     794         103 :         .allMatch(SubmitRequirementResult::fulfilled);
     795             :   }
     796             : 
     797             :   private void setSubmitter(ChangeData cd, ChangeInfo out) {
     798         103 :     Optional<PatchSetApproval> s = cd.getSubmitApproval();
     799         103 :     if (!s.isPresent()) {
     800         103 :       return;
     801             :     }
     802          55 :     out.setSubmitted(s.get().granted(), accountLoader.get(s.get().accountId()));
     803          55 :   }
     804             : 
     805             :   private ImmutableList<ChangeMessageInfo> messages(ChangeData cd) {
     806         103 :     List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
     807         103 :     if (messages.isEmpty()) {
     808           5 :       return ImmutableList.of();
     809             :     }
     810             : 
     811         102 :     List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size());
     812         102 :     for (ChangeMessage message : messages) {
     813         102 :       result.add(createChangeMessageInfo(message, accountLoader));
     814         102 :     }
     815         102 :     return ImmutableList.copyOf(result);
     816             :   }
     817             : 
     818             :   private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
     819             :       throws PermissionBackendException {
     820             :     // Although this is called removableReviewers, this method also determines
     821             :     // which CCs are removable.
     822             :     //
     823             :     // For reviewers, we need to look at each approval, because the reviewer
     824             :     // should only be considered removable if *all* of their approvals can be
     825             :     // removed. First, add all reviewers with *any* removable approval to the
     826             :     // "removable" set. Along the way, if we encounter a non-removable approval,
     827             :     // add the reviewer to the "fixed" set. Before we return, remove all members
     828             :     // of "fixed" from "removable", because not all of their approvals can be
     829             :     // removed.
     830         103 :     Collection<LabelInfo> labels = out.labels.values();
     831         103 :     Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
     832         103 :     Set<Account.Id> removable = new HashSet<>();
     833             : 
     834             :     // Add all reviewers, which will later be removed if they are in the "fixed" set.
     835         103 :     removable.addAll(
     836         103 :         out.reviewers.getOrDefault(ReviewerState.REVIEWER, Collections.emptySet()).stream()
     837         103 :             .filter(a -> a._accountId != null)
     838         103 :             .map(a -> Account.id(a._accountId))
     839         103 :             .collect(Collectors.toSet()));
     840             : 
     841             :     // Check if the user has the permission to remove a reviewer. This means we can bypass the
     842             :     // testRemoveReviewer check for a specific reviewer in the loop saving potentially many
     843             :     // permission checks.
     844         103 :     boolean canRemoveAnyReviewer =
     845             :         permissionBackend
     846         103 :             .user(userProvider.get())
     847         103 :             .change(cd)
     848         103 :             .test(ChangePermission.REMOVE_REVIEWER);
     849         103 :     for (LabelInfo label : labels) {
     850         103 :       if (label.all == null) {
     851         102 :         continue;
     852             :       }
     853          71 :       for (ApprovalInfo ai : label.all) {
     854          71 :         Account.Id id = Account.id(ai._accountId);
     855             : 
     856          71 :         if (!canRemoveAnyReviewer
     857          71 :             && !removeReviewerControl.testRemoveReviewer(
     858          71 :                 cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) {
     859          49 :           fixed.add(id);
     860             :         }
     861          71 :       }
     862          71 :     }
     863             : 
     864             :     // CCs are simpler than reviewers. They are removable if the ChangeControl
     865             :     // would permit a non-negative approval by that account to be removed, in
     866             :     // which case add them to removable. We don't need to add unremovable CCs to
     867             :     // "fixed" because we only visit each CC once here.
     868         103 :     Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
     869         103 :     if (ccs != null) {
     870          25 :       for (AccountInfo ai : ccs) {
     871          25 :         if (ai._accountId != null) {
     872          25 :           Account.Id id = Account.id(ai._accountId);
     873          25 :           if (canRemoveAnyReviewer
     874          25 :               || removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) {
     875          25 :             removable.add(id);
     876             :           }
     877             :         }
     878          25 :       }
     879             :     }
     880             : 
     881             :     // Subtract any reviewers with non-removable approvals from the "removable"
     882             :     // set. This also subtracts any CCs that for some reason also hold
     883             :     // unremovable approvals.
     884         103 :     removable.removeAll(fixed);
     885             : 
     886         103 :     List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size());
     887         103 :     for (Account.Id id : removable) {
     888          72 :       result.add(accountLoader.get(id));
     889          72 :     }
     890             :     // Reviewers added by email are always removable
     891         103 :     for (Collection<AccountInfo> infos : out.reviewers.values()) {
     892          72 :       for (AccountInfo info : infos) {
     893          72 :         if (info._accountId == null) {
     894          10 :           result.add(info);
     895             :         }
     896          72 :       }
     897          72 :     }
     898         103 :     return result;
     899             :   }
     900             : 
     901             :   private Collection<AccountInfo> toAccountInfo(Collection<Account.Id> accounts) {
     902         103 :     return accounts.stream()
     903         103 :         .map(accountLoader::get)
     904         103 :         .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
     905         103 :         .collect(toList());
     906             :   }
     907             : 
     908             :   private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
     909         103 :     return addresses.stream()
     910         103 :         .map(a -> new AccountInfo(a.name(), a.email()))
     911         103 :         .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
     912         103 :         .collect(toList());
     913             :   }
     914             : 
     915             :   private Map<PatchSet.Id, PatchSet> loadPatchSets(
     916             :       ChangeData cd, Optional<PatchSet.Id> limitToPsId) {
     917             :     Collection<PatchSet> src;
     918         103 :     if (has(ALL_REVISIONS) || has(MESSAGES)) {
     919         103 :       src = cd.patchSets();
     920             :     } else {
     921             :       PatchSet ps;
     922          28 :       if (limitToPsId.isPresent()) {
     923           3 :         ps = cd.patchSet(limitToPsId.get());
     924           3 :         if (ps == null) {
     925           0 :           throw new StorageException("missing patch set " + limitToPsId.get());
     926             :         }
     927             :       } else {
     928          26 :         ps = cd.currentPatchSet();
     929          26 :         if (ps == null) {
     930           0 :           throw new StorageException("missing current patch set for change " + cd.getId());
     931             :         }
     932             :       }
     933          28 :       src = Collections.singletonList(ps);
     934             :     }
     935             :     // Sort by patch set ID in increasing order to have a stable output.
     936         103 :     ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> map = ImmutableSortedMap.naturalOrder();
     937         103 :     for (PatchSet patchSet : src) {
     938         103 :       map.put(patchSet.id(), patchSet);
     939         103 :     }
     940         103 :     return map.build();
     941             :   }
     942             : 
     943             :   private List<PluginDefinedInfo> getPluginInfos(ChangeData cd) {
     944         103 :     return getPluginInfos(Collections.singleton(cd)).get(cd.getId());
     945             :   }
     946             : 
     947             :   private ImmutableListMultimap<Change.Id, PluginDefinedInfo> getPluginInfos(
     948             :       Collection<ChangeData> cds) {
     949         103 :     if (pluginDefinedInfosFactory.isPresent()) {
     950          70 :       return pluginDefinedInfosFactory.get().createPluginDefinedInfos(cds);
     951             :     }
     952         103 :     return ImmutableListMultimap.of();
     953             :   }
     954             : }

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