All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.change.ChangeJson Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.change;

import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_MERGEABLE;
import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
import static java.util.stream.Collectors.toList;

import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRecord.Status;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.api.changes.FixInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.ProblemInfo;
import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.mail.Address;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
import com.google.gerrit.server.query.change.PluginDefinedAttributesFactory;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;

/**
 * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
 *
 * 

This is intended to be used on request scope, but may be used for converting multiple {@link * ChangeData} objects from different sources. */ public class ChangeJson { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT = ChangeField.SUBMIT_RULE_OPTIONS_LENIENT.toBuilder().build(); public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT = ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build(); static final ImmutableSet REQUIRE_LAZY_LOAD = ImmutableSet.of( ALL_COMMITS, ALL_REVISIONS, CHANGE_ACTIONS, CHECK, COMMIT_FOOTERS, CURRENT_ACTIONS, CURRENT_COMMIT, MESSAGES); @Singleton public static class Factory { private final AssistedFactory factory; @Inject Factory(AssistedFactory factory) { this.factory = factory; } public ChangeJson noOptions() { return create(ImmutableSet.of()); } public ChangeJson create(Iterable options) { return factory.create(options, Optional.empty()); } public ChangeJson create( Iterable options, PluginDefinedAttributesFactory pluginDefinedAttributesFactory) { return factory.create(options, Optional.of(pluginDefinedAttributesFactory)); } public ChangeJson create(ListChangesOption first, ListChangesOption... rest) { return create(Sets.immutableEnumSet(first, rest)); } } public interface AssistedFactory { ChangeJson create( Iterable options, Optional pluginDefinedAttributesFactory); } @Singleton private static class Metrics { private final Timer0 toChangeInfoLatency; private final Timer0 toChangeInfosLatency; private final Timer0 formatQueryResultsLatency; @Inject Metrics(MetricMaker metricMaker) { toChangeInfoLatency = metricMaker.newTimer( "http/server/rest_api/change_json/to_change_info_latency", new Description("Latency for toChangeInfo invocations in ChangeJson") .setCumulative() .setUnit(Units.MILLISECONDS)); toChangeInfosLatency = metricMaker.newTimer( "http/server/rest_api/change_json/to_change_infos_latency", new Description("Latency for toChangeInfos invocations in ChangeJson") .setCumulative() .setUnit(Units.MILLISECONDS)); formatQueryResultsLatency = metricMaker.newTimer( "http/server/rest_api/change_json/format_query_results_latency", new Description("Latency for formatQueryResults invocations in ChangeJson") .setCumulative() .setUnit(Units.MILLISECONDS)); } } private final Provider db; private final Provider userProvider; private final PermissionBackend permissionBackend; private final ChangeData.Factory changeDataFactory; private final AccountLoader.Factory accountLoaderFactory; private final ImmutableSet options; private final ChangeMessagesUtil cmUtil; private final Provider checkerProvider; private final ActionJson actionJson; private final ChangeNotes.Factory notesFactory; private final LabelsJson labelsJson; private final RemoveReviewerControl removeReviewerControl; private final TrackingFooters trackingFooters; private final Metrics metrics; private final RevisionJson revisionJson; private final Optional pluginDefinedAttributesFactory; private final boolean lazyLoad; private AccountLoader accountLoader; private FixInput fix; @Inject ChangeJson( Provider db, Provider user, PermissionBackend permissionBackend, ChangeData.Factory cdf, AccountLoader.Factory ailf, ChangeMessagesUtil cmUtil, Provider checkerProvider, ActionJson actionJson, ChangeNotes.Factory notesFactory, LabelsJson.Factory labelsJsonFactory, RemoveReviewerControl removeReviewerControl, TrackingFooters trackingFooters, Metrics metrics, RevisionJson.Factory revisionJsonFactory, @Assisted Iterable options, @Assisted Optional pluginDefinedAttributesFactory) { this.db = db; this.userProvider = user; this.changeDataFactory = cdf; this.permissionBackend = permissionBackend; this.accountLoaderFactory = ailf; this.cmUtil = cmUtil; this.checkerProvider = checkerProvider; this.actionJson = actionJson; this.notesFactory = notesFactory; this.labelsJson = labelsJsonFactory.create(options); this.removeReviewerControl = removeReviewerControl; this.trackingFooters = trackingFooters; this.metrics = metrics; this.revisionJson = revisionJsonFactory.create(options); this.options = Sets.immutableEnumSet(options); this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD); this.pluginDefinedAttributesFactory = pluginDefinedAttributesFactory; logger.atFine().log("options = %s", options); } public ChangeJson fix(FixInput fix) { this.fix = fix; return this; } public ChangeInfo format(ChangeResource rsrc) throws OrmException { return format(changeDataFactory.create(db.get(), rsrc.getNotes())); } public ChangeInfo format(Change change) throws OrmException { return format(changeDataFactory.create(db.get(), change)); } public ChangeInfo format(Project.NameKey project, Change.Id id) throws OrmException { return format(project, id, ChangeInfo::new); } public ChangeInfo format(ChangeData cd) throws OrmException { return format(cd, Optional.empty(), true, ChangeInfo::new); } public ChangeInfo format(RevisionResource rsrc) throws OrmException { ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes()); return format(cd, Optional.of(rsrc.getPatchSet().getId()), true, ChangeInfo::new); } public List> format(List> in) throws PermissionBackendException { try (Timer0.Context ignored = metrics.formatQueryResultsLatency.start()) { accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); List> res = new ArrayList<>(in.size()); Map cache = Maps.newHashMapWithExpectedSize(in.size()); for (QueryResult r : in) { List infos = toChangeInfos(r.entities(), cache); infos.forEach(c -> cache.put(new Change.Id(c._number), c)); if (!infos.isEmpty() && r.more()) { infos.get(infos.size() - 1)._moreChanges = true; } res.add(infos); } accountLoader.fill(); return res; } } public List format(Collection in) throws OrmException, PermissionBackendException { accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); ensureLoaded(in); List out = new ArrayList<>(in.size()); for (ChangeData cd : in) { out.add(format(cd, Optional.empty(), false, ChangeInfo::new)); } accountLoader.fill(); return out; } public I format( Project.NameKey project, Change.Id id, Supplier changeInfoSupplier) throws OrmException { ChangeNotes notes; try { notes = notesFactory.createChecked(db.get(), project, id); } catch (OrmException e) { if (!has(CHECK)) { throw e; } return checkOnly(changeDataFactory.create(db.get(), project, id), changeInfoSupplier); } return format( changeDataFactory.create(db.get(), notes), Optional.empty(), true, changeInfoSupplier); } private static Collection requirementsFor(ChangeData cd) { Collection reqInfos = new ArrayList<>(); for (SubmitRecord submitRecord : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) { if (submitRecord.requirements == null) { continue; } for (SubmitRequirement requirement : submitRecord.requirements) { reqInfos.add(requirementToInfo(requirement, submitRecord.status)); } } return reqInfos; } private static SubmitRequirementInfo requirementToInfo(SubmitRequirement req, Status status) { return new SubmitRequirementInfo(status.name(), req.fallbackText(), req.type(), req.data()); } private static void finish(ChangeInfo info) { info.id = Joiner.on('~') .join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId)); } private static boolean containsAnyOf( ImmutableSet set, ImmutableSet toFind) { return !Sets.intersection(toFind, set).isEmpty(); } private I format( ChangeData cd, Optional limitToPsId, boolean fillAccountLoader, Supplier changeInfoSupplier) throws OrmException { try { if (fillAccountLoader) { accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); I res = toChangeInfo(cd, limitToPsId, changeInfoSupplier); accountLoader.fill(); return res; } return toChangeInfo(cd, limitToPsId, changeInfoSupplier); } catch (PatchListNotAvailableException | GpgException | OrmException | IOException | PermissionBackendException | RuntimeException e) { if (!has(CHECK)) { Throwables.throwIfInstanceOf(e, OrmException.class); throw new OrmException(e); } return checkOnly(cd, changeInfoSupplier); } } private void ensureLoaded(Iterable all) throws OrmException { if (lazyLoad) { ChangeData.ensureChangeLoaded(all); if (has(ALL_REVISIONS)) { ChangeData.ensureAllPatchSetsLoaded(all); } else if (has(CURRENT_REVISION) || has(MESSAGES)) { ChangeData.ensureCurrentPatchSetLoaded(all); } if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) { ChangeData.ensureReviewedByLoadedForOpenChanges(all); } ChangeData.ensureCurrentApprovalsLoaded(all); } else { for (ChangeData cd : all) { cd.setLazyLoad(false); } } } private boolean has(ListChangesOption option) { return options.contains(option); } private List toChangeInfos( List changes, Map cache) { try (Timer0.Context ignored = metrics.toChangeInfosLatency.start()) { List changeInfos = new ArrayList<>(changes.size()); for (ChangeData cd : changes) { ChangeInfo i = cache.get(cd.getId()); if (i != null) { continue; } try { ensureLoaded(Collections.singleton(cd)); changeInfos.add(format(cd, Optional.empty(), false, ChangeInfo::new)); } catch (OrmException | RuntimeException e) { logger.atWarning().withCause(e).log( "Omitting corrupt change %s from results", cd.getId()); } } return changeInfos; } } private I checkOnly(ChangeData cd, Supplier changeInfoSupplier) { ChangeNotes notes; try { notes = cd.notes(); } catch (OrmException e) { String msg = "Error loading change"; logger.atWarning().withCause(e).log(msg + " %s", cd.getId()); I info = changeInfoSupplier.get(); info._number = cd.getId().get(); ProblemInfo p = new ProblemInfo(); p.message = msg; info.problems = Lists.newArrayList(p); return info; } ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix); I info = changeInfoSupplier.get(); Change c = result.change(); if (c != null) { info.project = c.getProject().get(); info.branch = c.getDest().getShortName(); info.topic = c.getTopic(); info.changeId = c.getKey().get(); info.subject = c.getSubject(); info.status = c.getStatus().asChangeStatus(); info.owner = new AccountInfo(c.getOwner().get()); info.created = c.getCreatedOn(); info.updated = c.getLastUpdatedOn(); info._number = c.getId().get(); info.problems = result.problems(); info.isPrivate = c.isPrivate() ? true : null; info.workInProgress = c.isWorkInProgress() ? true : null; info.hasReviewStarted = c.hasReviewStarted(); finish(info); } else { info._number = result.id().get(); info.problems = result.problems(); } return info; } private I toChangeInfo( ChangeData cd, Optional limitToPsId, Supplier changeInfoSupplier) throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException, IOException { try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) { return toChangeInfoImpl(cd, limitToPsId, changeInfoSupplier); } } private I toChangeInfoImpl( ChangeData cd, Optional limitToPsId, Supplier changeInfoSupplier) throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException, IOException { I out = changeInfoSupplier.get(); CurrentUser user = userProvider.get(); if (has(CHECK)) { out.problems = checkerProvider.get().check(cd.notes(), fix).problems(); // If any problems were fixed, the ChangeData needs to be reloaded. for (ProblemInfo p : out.problems) { if (p.status == ProblemInfo.Status.FIXED) { cd = changeDataFactory.create(cd.db(), cd.project(), cd.getId()); break; } } } Change in = cd.change(); out.project = in.getProject().get(); out.branch = in.getDest().getShortName(); out.topic = in.getTopic(); out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null; out.hashtags = cd.hashtags(); out.changeId = in.getKey().get(); if (in.getStatus().isOpen()) { SubmitTypeRecord str = cd.submitTypeRecord(); if (str.isOk()) { out.submitType = str.type; } if (!has(SKIP_MERGEABLE)) { out.mergeable = cd.isMergeable(); } if (has(SUBMITTABLE)) { out.submittable = submittable(cd); } } Optional changedLines = cd.changedLines(); if (changedLines.isPresent()) { out.insertions = changedLines.get().insertions; out.deletions = changedLines.get().deletions; } out.isPrivate = in.isPrivate() ? true : null; out.workInProgress = in.isWorkInProgress() ? true : null; out.hasReviewStarted = in.hasReviewStarted(); out.subject = in.getSubject(); out.status = in.getStatus().asChangeStatus(); out.owner = accountLoader.get(in.getOwner()); out.created = in.getCreatedOn(); out.updated = in.getLastUpdatedOn(); out._number = in.getId().get(); out.unresolvedCommentCount = cd.unresolvedCommentCount(); if (user.isIdentifiedUser()) { Collection stars = cd.stars(user.getAccountId()); out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null; if (!stars.isEmpty()) { out.stars = stars; } } if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) { out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null; } out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS)); out.requirements = requirementsFor(cd); if (out.labels != null && has(DETAILED_LABELS)) { // If limited to specific patch sets but not the current patch set, don't // list permitted labels, since users can't vote on those patch sets. if (user.isIdentifiedUser() && (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) { out.permittedLabels = cd.change().getStatus() != Change.Status.ABANDONED ? labelsJson.permittedLabels(user.getAccountId(), cd) : ImmutableMap.of(); } out.reviewers = reviewerMap(cd.reviewers(), cd.reviewersByEmail(), false); out.pendingReviewers = reviewerMap(cd.pendingReviewers(), cd.pendingReviewersByEmail(), true); out.removableReviewers = removableReviewers(cd, out); } setSubmitter(cd, out); if (pluginDefinedAttributesFactory.isPresent()) { out.plugins = pluginDefinedAttributesFactory.get().create(cd); } out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null; if (has(REVIEWER_UPDATES)) { out.reviewerUpdates = reviewerUpdates(cd); } boolean needMessages = has(MESSAGES); boolean needRevisions = has(ALL_REVISIONS) || has(CURRENT_REVISION) || limitToPsId.isPresent(); Map src; if (needMessages || needRevisions) { src = loadPatchSets(cd, limitToPsId); } else { src = null; } if (needMessages) { out.messages = messages(cd); } finish(out); // This block must come after the ChangeInfo is mostly populated, since // it will be passed to ActionVisitors as-is. if (needRevisions) { out.revisions = revisionJson.getRevisions(accountLoader, cd, src, limitToPsId, out); if (out.revisions != null) { for (Map.Entry entry : out.revisions.entrySet()) { if (entry.getValue().isCurrent) { out.currentRevision = entry.getKey(); break; } } } } if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) { actionJson.addChangeActions(out, cd.notes()); } if (has(TRACKING_IDS)) { ListMultimap set = trackingFooters.extract(cd.commitFooters()); out.trackingIds = set.entries() .stream() .map(e -> new TrackingIdInfo(e.getKey(), e.getValue())) .collect(toList()); } return out; } private Map> reviewerMap( ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, boolean includeRemoved) { Map> reviewerMap = new HashMap<>(); for (ReviewerStateInternal state : ReviewerStateInternal.values()) { if (!includeRemoved && state == ReviewerStateInternal.REMOVED) { continue; } Collection reviewersByState = toAccountInfo(reviewers.byState(state)); reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state))); if (!reviewersByState.isEmpty()) { reviewerMap.put(state.asReviewerState(), reviewersByState); } } return reviewerMap; } private Collection reviewerUpdates(ChangeData cd) throws OrmException { List reviewerUpdates = cd.reviewerUpdates(); List result = new ArrayList<>(reviewerUpdates.size()); for (ReviewerStatusUpdate c : reviewerUpdates) { ReviewerUpdateInfo change = new ReviewerUpdateInfo(); change.updated = c.date(); change.state = c.state().asReviewerState(); change.updatedBy = accountLoader.get(c.updatedBy()); change.reviewer = accountLoader.get(c.reviewer()); result.add(change); } return result; } private boolean submittable(ChangeData cd) { return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)); } private void setSubmitter(ChangeData cd, ChangeInfo out) throws OrmException { Optional s = cd.getSubmitApproval(); if (!s.isPresent()) { return; } out.submitted = s.get().getGranted(); out.submitter = accountLoader.get(s.get().getAccountId()); } private Collection messages(ChangeData cd) throws OrmException { List messages = cmUtil.byChange(db.get(), cd.notes()); if (messages.isEmpty()) { return Collections.emptyList(); } List result = Lists.newArrayListWithCapacity(messages.size()); for (ChangeMessage message : messages) { result.add(createChangeMessageInfo(message, accountLoader)); } return result; } private Collection removableReviewers(ChangeData cd, ChangeInfo out) throws PermissionBackendException, OrmException { // Although this is called removableReviewers, this method also determines // which CCs are removable. // // For reviewers, we need to look at each approval, because the reviewer // should only be considered removable if *all* of their approvals can be // removed. First, add all reviewers with *any* removable approval to the // "removable" set. Along the way, if we encounter a non-removable approval, // add the reviewer to the "fixed" set. Before we return, remove all members // of "fixed" from "removable", because not all of their approvals can be // removed. Collection labels = out.labels.values(); Set fixed = Sets.newHashSetWithExpectedSize(labels.size()); Set removable = Sets.newHashSetWithExpectedSize(labels.size()); // Check if the user has the permission to remove a reviewer. This means we can bypass the // testRemoveReviewer check for a specific reviewer in the loop saving potentially many // permission checks. boolean canRemoveAnyReviewer = permissionBackendForChange(userProvider.get(), cd).test(ChangePermission.REMOVE_REVIEWER); for (LabelInfo label : labels) { if (label.all == null) { continue; } for (ApprovalInfo ai : label.all) { Account.Id id = new Account.Id(ai._accountId); if (canRemoveAnyReviewer || removeReviewerControl.testRemoveReviewer( cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) { removable.add(id); } else { fixed.add(id); } } } // CCs are simpler than reviewers. They are removable if the ChangeControl // would permit a non-negative approval by that account to be removed, in // which case add them to removable. We don't need to add unremovable CCs to // "fixed" because we only visit each CC once here. Collection ccs = out.reviewers.get(ReviewerState.CC); if (ccs != null) { for (AccountInfo ai : ccs) { if (ai._accountId != null) { Account.Id id = new Account.Id(ai._accountId); if (canRemoveAnyReviewer || removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) { removable.add(id); } } } } // Subtract any reviewers with non-removable approvals from the "removable" // set. This also subtracts any CCs that for some reason also hold // unremovable approvals. removable.removeAll(fixed); List result = Lists.newArrayListWithCapacity(removable.size()); for (Account.Id id : removable) { result.add(accountLoader.get(id)); } // Reviewers added by email are always removable for (Collection infos : out.reviewers.values()) { for (AccountInfo info : infos) { if (info._accountId == null) { result.add(info); } } } return result; } private Collection toAccountInfo(Collection accounts) { return accounts .stream() .map(accountLoader::get) .sorted(AccountInfoComparator.ORDER_NULLS_FIRST) .collect(toList()); } private Collection toAccountInfoByEmail(Collection

addresses) { return addresses .stream() .map(a -> new AccountInfo(a.getName(), a.getEmail())) .sorted(AccountInfoComparator.ORDER_NULLS_FIRST) .collect(toList()); } private Map loadPatchSets(ChangeData cd, Optional limitToPsId) throws OrmException { Collection src; if (has(ALL_REVISIONS) || has(MESSAGES)) { src = cd.patchSets(); } else { PatchSet ps; if (limitToPsId.isPresent()) { ps = cd.patchSet(limitToPsId.get()); if (ps == null) { throw new OrmException("missing patch set " + limitToPsId.get()); } } else { ps = cd.currentPatchSet(); if (ps == null) { throw new OrmException("missing current patch set for change " + cd.getId()); } } src = Collections.singletonList(ps); } Map map = Maps.newHashMapWithExpectedSize(src.size()); for (PatchSet patchSet : src) { map.put(patchSet.getId(), patchSet); } return map; } /** * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed * from either an index-backed or a database-backed {@link ChangeData} depending on {@code * lazyload}. */ private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd) throws OrmException { PermissionBackend.WithUser withUser = permissionBackend.user(user).database(db); return lazyLoad ? withUser.change(cd) : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change())); } }