LCOV - code coverage report
Current view: top level - server/restapi/change - PostReview.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 435 452 96.2 %
Date: 2022-11-19 15:00:39 Functions: 48 48 100.0 %

          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.restapi.change;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      19             : import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
      20             : import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
      21             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      22             : import static java.nio.charset.StandardCharsets.UTF_8;
      23             : import static java.util.stream.Collectors.groupingBy;
      24             : import static java.util.stream.Collectors.toList;
      25             : import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
      26             : 
      27             : import com.google.auto.value.AutoValue;
      28             : import com.google.common.base.Strings;
      29             : import com.google.common.collect.Lists;
      30             : import com.google.common.collect.Maps;
      31             : import com.google.common.collect.Ordering;
      32             : import com.google.common.collect.Streams;
      33             : import com.google.common.flogger.FluentLogger;
      34             : import com.google.common.hash.HashCode;
      35             : import com.google.common.hash.Hashing;
      36             : import com.google.gerrit.common.Nullable;
      37             : import com.google.gerrit.entities.Account;
      38             : import com.google.gerrit.entities.Address;
      39             : import com.google.gerrit.entities.Change;
      40             : import com.google.gerrit.entities.Comment;
      41             : import com.google.gerrit.entities.HumanComment;
      42             : import com.google.gerrit.entities.LabelType;
      43             : import com.google.gerrit.entities.LabelTypes;
      44             : import com.google.gerrit.entities.Patch;
      45             : import com.google.gerrit.entities.PatchSet;
      46             : import com.google.gerrit.extensions.api.changes.NotifyHandling;
      47             : import com.google.gerrit.extensions.api.changes.ReviewInput;
      48             : import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
      49             : import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
      50             : import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
      51             : import com.google.gerrit.extensions.api.changes.ReviewResult;
      52             : import com.google.gerrit.extensions.api.changes.ReviewerInput;
      53             : import com.google.gerrit.extensions.api.changes.ReviewerResult;
      54             : import com.google.gerrit.extensions.client.Comment.Range;
      55             : import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
      56             : import com.google.gerrit.extensions.client.ReviewerState;
      57             : import com.google.gerrit.extensions.client.Side;
      58             : import com.google.gerrit.extensions.common.FixReplacementInfo;
      59             : import com.google.gerrit.extensions.common.FixSuggestionInfo;
      60             : import com.google.gerrit.extensions.restapi.AuthException;
      61             : import com.google.gerrit.extensions.restapi.BadRequestException;
      62             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      63             : import com.google.gerrit.extensions.restapi.Response;
      64             : import com.google.gerrit.extensions.restapi.RestApiException;
      65             : import com.google.gerrit.extensions.restapi.RestModifyView;
      66             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      67             : import com.google.gerrit.metrics.Counter1;
      68             : import com.google.gerrit.metrics.Description;
      69             : import com.google.gerrit.metrics.Field;
      70             : import com.google.gerrit.metrics.MetricMaker;
      71             : import com.google.gerrit.server.ChangeMessagesUtil;
      72             : import com.google.gerrit.server.CommentsUtil;
      73             : import com.google.gerrit.server.CurrentUser;
      74             : import com.google.gerrit.server.IdentifiedUser;
      75             : import com.google.gerrit.server.ReviewerSet;
      76             : import com.google.gerrit.server.account.AccountCache;
      77             : import com.google.gerrit.server.account.AccountResolver;
      78             : import com.google.gerrit.server.account.AccountState;
      79             : import com.google.gerrit.server.approval.ApprovalsUtil;
      80             : import com.google.gerrit.server.change.ChangeResource;
      81             : import com.google.gerrit.server.change.ModifyReviewersEmail;
      82             : import com.google.gerrit.server.change.NotifyResolver;
      83             : import com.google.gerrit.server.change.ReviewerModifier;
      84             : import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
      85             : import com.google.gerrit.server.change.ReviewerOp.Result;
      86             : import com.google.gerrit.server.change.RevisionResource;
      87             : import com.google.gerrit.server.change.WorkInProgressOp;
      88             : import com.google.gerrit.server.config.GerritServerConfig;
      89             : import com.google.gerrit.server.extensions.events.ReviewerAdded;
      90             : import com.google.gerrit.server.logging.Metadata;
      91             : import com.google.gerrit.server.logging.TraceContext;
      92             : import com.google.gerrit.server.notedb.ChangeNotes;
      93             : import com.google.gerrit.server.patch.DiffSummary;
      94             : import com.google.gerrit.server.patch.DiffSummaryKey;
      95             : import com.google.gerrit.server.patch.PatchListCache;
      96             : import com.google.gerrit.server.patch.PatchListKey;
      97             : import com.google.gerrit.server.patch.PatchListNotAvailableException;
      98             : import com.google.gerrit.server.permissions.ChangePermission;
      99             : import com.google.gerrit.server.permissions.LabelPermission;
     100             : import com.google.gerrit.server.permissions.PermissionBackend;
     101             : import com.google.gerrit.server.permissions.PermissionBackendException;
     102             : import com.google.gerrit.server.project.ProjectCache;
     103             : import com.google.gerrit.server.project.ProjectState;
     104             : import com.google.gerrit.server.query.change.ChangeData;
     105             : import com.google.gerrit.server.update.BatchUpdate;
     106             : import com.google.gerrit.server.update.UpdateException;
     107             : import com.google.gerrit.server.util.time.TimeUtil;
     108             : import com.google.inject.Inject;
     109             : import com.google.inject.Singleton;
     110             : import java.io.IOException;
     111             : import java.time.Instant;
     112             : import java.util.ArrayList;
     113             : import java.util.HashMap;
     114             : import java.util.HashSet;
     115             : import java.util.Iterator;
     116             : import java.util.List;
     117             : import java.util.Map;
     118             : import java.util.Objects;
     119             : import java.util.Optional;
     120             : import java.util.Set;
     121             : import java.util.stream.Collectors;
     122             : import org.eclipse.jgit.errors.ConfigInvalidException;
     123             : import org.eclipse.jgit.lib.Config;
     124             : import org.eclipse.jgit.lib.ObjectId;
     125             : 
     126             : @Singleton
     127             : public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
     128         145 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     129             : 
     130             :   @Singleton
     131             :   private static class Metrics {
     132             :     final Counter1<String> draftHandling;
     133             : 
     134             :     @Inject
     135         145 :     Metrics(MetricMaker metricMaker) {
     136         145 :       draftHandling =
     137         145 :           metricMaker.newCounter(
     138             :               "change/post_review/draft_handling",
     139             :               new Description(
     140             :                       "Total number of draft handling option "
     141             :                           + "(KEEP, PUBLISH, PUBLISH_ALL_REVISIONS) "
     142             :                           + "selected by users while posting a review.")
     143         145 :                   .setRate()
     144         145 :                   .setUnit("count"),
     145         145 :               Field.ofString("type", Metadata.Builder::eventType)
     146         145 :                   .description(
     147             :                       "The type of the draft handling option"
     148             :                           + " (KEEP, PUBLISH, PUBLISH_ALL_REVISIONS).")
     149         145 :                   .build());
     150         145 :     }
     151             :   }
     152             : 
     153             :   private static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
     154             :   public static final String ERROR_WIP_READY_MUTUALLY_EXCLUSIVE =
     155             :       "work_in_progress and ready are mutually exclusive";
     156             : 
     157             :   private final BatchUpdate.Factory updateFactory;
     158             :   private final PostReviewOp.Factory postReviewOpFactory;
     159             :   private final PostReviewCopyApprovalsOpFactory postReviewCopyApprovalsOpFactory;
     160             :   private final ChangeResource.Factory changeResourceFactory;
     161             :   private final ChangeData.Factory changeDataFactory;
     162             :   private final AccountCache accountCache;
     163             :   private final ApprovalsUtil approvalsUtil;
     164             :   private final CommentsUtil commentsUtil;
     165             :   private final PatchListCache patchListCache;
     166             :   private final AccountResolver accountResolver;
     167             :   private final ReviewerModifier reviewerModifier;
     168             :   private final Metrics metrics;
     169             :   private final ModifyReviewersEmail modifyReviewersEmail;
     170             :   private final NotifyResolver notifyResolver;
     171             :   private final WorkInProgressOp.Factory workInProgressOpFactory;
     172             :   private final ProjectCache projectCache;
     173             :   private final PermissionBackend permissionBackend;
     174             : 
     175             :   private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
     176             :   private final ReviewerAdded reviewerAdded;
     177             :   private final boolean strictLabels;
     178             : 
     179             :   @Inject
     180             :   PostReview(
     181             :       BatchUpdate.Factory updateFactory,
     182             :       PostReviewOp.Factory postReviewOpFactory,
     183             :       PostReviewCopyApprovalsOpFactory postReviewCopyApprovalsOpFactory,
     184             :       ChangeResource.Factory changeResourceFactory,
     185             :       ChangeData.Factory changeDataFactory,
     186             :       AccountCache accountCache,
     187             :       ApprovalsUtil approvalsUtil,
     188             :       CommentsUtil commentsUtil,
     189             :       PatchListCache patchListCache,
     190             :       AccountResolver accountResolver,
     191             :       ReviewerModifier reviewerModifier,
     192             :       Metrics metrics,
     193             :       ModifyReviewersEmail modifyReviewersEmail,
     194             :       NotifyResolver notifyResolver,
     195             :       @GerritServerConfig Config gerritConfig,
     196             :       WorkInProgressOp.Factory workInProgressOpFactory,
     197             :       ProjectCache projectCache,
     198             :       PermissionBackend permissionBackend,
     199             :       ReplyAttentionSetUpdates replyAttentionSetUpdates,
     200         145 :       ReviewerAdded reviewerAdded) {
     201         145 :     this.updateFactory = updateFactory;
     202         145 :     this.postReviewOpFactory = postReviewOpFactory;
     203         145 :     this.postReviewCopyApprovalsOpFactory = postReviewCopyApprovalsOpFactory;
     204         145 :     this.changeResourceFactory = changeResourceFactory;
     205         145 :     this.changeDataFactory = changeDataFactory;
     206         145 :     this.accountCache = accountCache;
     207         145 :     this.commentsUtil = commentsUtil;
     208         145 :     this.patchListCache = patchListCache;
     209         145 :     this.approvalsUtil = approvalsUtil;
     210         145 :     this.accountResolver = accountResolver;
     211         145 :     this.reviewerModifier = reviewerModifier;
     212         145 :     this.metrics = metrics;
     213         145 :     this.modifyReviewersEmail = modifyReviewersEmail;
     214         145 :     this.notifyResolver = notifyResolver;
     215         145 :     this.workInProgressOpFactory = workInProgressOpFactory;
     216         145 :     this.projectCache = projectCache;
     217         145 :     this.permissionBackend = permissionBackend;
     218         145 :     this.replyAttentionSetUpdates = replyAttentionSetUpdates;
     219         145 :     this.reviewerAdded = reviewerAdded;
     220         145 :     this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
     221         145 :   }
     222             : 
     223             :   @Override
     224             :   public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
     225             :       throws RestApiException, UpdateException, IOException, PermissionBackendException,
     226             :           ConfigInvalidException, PatchListNotAvailableException {
     227          65 :     return apply(revision, input, TimeUtil.now());
     228             :   }
     229             : 
     230             :   public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Instant ts)
     231             :       throws RestApiException, UpdateException, IOException, PermissionBackendException,
     232             :           ConfigInvalidException, PatchListNotAvailableException {
     233             :     // Respect timestamp, but truncate at change created-on time.
     234          65 :     ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
     235          65 :     if (revision.getEdit().isPresent()) {
     236           0 :       throw new ResourceConflictException("cannot post review on edit");
     237             :     }
     238          65 :     ProjectState projectState =
     239          65 :         projectCache.get(revision.getProject()).orElseThrow(illegalState(revision.getProject()));
     240          65 :     LabelTypes labelTypes = projectState.getLabelTypes(revision.getNotes());
     241             : 
     242          65 :     logger.atFine().log("strict label checking is %s", (strictLabels ? "enabled" : "disabled"));
     243             : 
     244          65 :     metrics.draftHandling.increment(input.drafts == null ? "N/A" : input.drafts.name());
     245          65 :     input.drafts = firstNonNull(input.drafts, DraftHandling.KEEP);
     246          65 :     logger.atFine().log("draft handling = %s", input.drafts);
     247             : 
     248          65 :     if (input.onBehalfOf != null) {
     249           2 :       revision = onBehalfOf(revision, labelTypes, input);
     250             :     }
     251          65 :     if (input.labels != null) {
     252          58 :       checkLabels(revision, labelTypes, input.labels);
     253             :     }
     254          65 :     if (input.comments != null) {
     255          18 :       input.comments = cleanUpComments(input.comments);
     256          18 :       checkComments(revision, input.comments);
     257             :     }
     258          65 :     if (input.draftIdsToPublish != null) {
     259           1 :       checkDraftIds(revision, input.draftIdsToPublish, input.drafts);
     260             :     }
     261          65 :     if (input.robotComments != null) {
     262           7 :       input.robotComments = cleanUpComments(input.robotComments);
     263           7 :       checkRobotComments(revision, input.robotComments);
     264             :     }
     265             : 
     266          65 :     if (input.notify == null) {
     267          65 :       input.notify = defaultNotify(revision.getChange(), input);
     268             :     }
     269          65 :     logger.atFine().log("notify handling = %s", input.notify);
     270             : 
     271          65 :     Map<String, ReviewerResult> reviewerJsonResults = null;
     272          65 :     List<ReviewerModification> reviewerResults = Lists.newArrayList();
     273          65 :     boolean hasError = false;
     274          65 :     boolean confirm = false;
     275          65 :     if (input.reviewers != null) {
     276          11 :       reviewerJsonResults = Maps.newHashMap();
     277          11 :       for (ReviewerInput reviewerInput : input.reviewers) {
     278          11 :         ReviewerModification result =
     279          11 :             reviewerModifier.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true);
     280          11 :         reviewerJsonResults.put(reviewerInput.reviewer, result.result);
     281          11 :         if (result.result.error != null) {
     282           2 :           logger.atFine().log(
     283             :               "Adding %s as reviewer failed: %s", reviewerInput.reviewer, result.result.error);
     284           2 :           hasError = true;
     285           2 :           continue;
     286             :         }
     287          11 :         if (result.result.confirm != null) {
     288           0 :           logger.atFine().log(
     289             :               "Adding %s as reviewer requires confirmation", reviewerInput.reviewer);
     290           0 :           confirm = true;
     291           0 :           continue;
     292             :         }
     293          11 :         logger.atFine().log("Adding %s as reviewer was prepared", reviewerInput.reviewer);
     294          11 :         reviewerResults.add(result);
     295          11 :       }
     296             :     }
     297             : 
     298          65 :     ReviewResult output = new ReviewResult();
     299          65 :     output.reviewers = reviewerJsonResults;
     300          65 :     if (hasError || confirm) {
     301           2 :       output.error = ERROR_ADDING_REVIEWER;
     302           2 :       return Response.withStatusCode(SC_BAD_REQUEST, output);
     303             :     }
     304          65 :     output.labels = input.labels;
     305             : 
     306             :     // Notify based on ReviewInput, ignoring the notify settings from any ReviewerInputs.
     307          65 :     NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);
     308             : 
     309          65 :     try (BatchUpdate bu =
     310          65 :         updateFactory.create(revision.getChange().getProject(), revision.getUser(), ts)) {
     311          65 :       bu.setNotify(notify);
     312             : 
     313          65 :       Account account = revision.getUser().asIdentifiedUser().getAccount();
     314          65 :       boolean ccOrReviewer = false;
     315          65 :       if (input.labels != null && !input.labels.isEmpty()) {
     316          58 :         ccOrReviewer = input.labels.values().stream().anyMatch(v -> v != 0);
     317          58 :         if (ccOrReviewer) {
     318          58 :           logger.atFine().log("calling user is cc/reviewer on the change due to voting on a label");
     319             :         }
     320             :       }
     321             : 
     322          65 :       if (!ccOrReviewer) {
     323             :         // Check if user was already CCed or reviewing prior to this review.
     324          27 :         ReviewerSet currentReviewers =
     325          27 :             approvalsUtil.getReviewers(revision.getChangeResource().getNotes());
     326          27 :         ccOrReviewer = currentReviewers.all().contains(account.id());
     327          27 :         if (ccOrReviewer) {
     328          10 :           logger.atFine().log("calling user is already cc/reviewer on the change");
     329             :         }
     330             :       }
     331             : 
     332             :       // Apply reviewer changes first. Revision emails should be sent to the
     333             :       // updated set of reviewers. Also keep track of whether the user added
     334             :       // themselves as a reviewer or to the CC list.
     335          65 :       logger.atFine().log("adding reviewer additions");
     336          65 :       for (ReviewerModification reviewerResult : reviewerResults) {
     337          11 :         reviewerResult.op.suppressEmail(); // Send a single batch email below.
     338          11 :         reviewerResult.op.suppressEvent(); // Send events below, if possible as batch.
     339          11 :         bu.addOp(revision.getChange().getId(), reviewerResult.op);
     340          11 :         if (!ccOrReviewer && reviewerResult.reviewers.contains(account)) {
     341           3 :           logger.atFine().log("calling user is explicitly added as reviewer or CC");
     342           3 :           ccOrReviewer = true;
     343             :         }
     344          11 :       }
     345             : 
     346          65 :       if (!ccOrReviewer) {
     347             :         // User posting this review isn't currently in the reviewer or CC list,
     348             :         // isn't being explicitly added, and isn't voting on any label.
     349             :         // Automatically CC them on this change so they receive replies.
     350          23 :         logger.atFine().log("CCing calling user");
     351          23 :         ReviewerModification selfAddition =
     352          23 :             reviewerModifier.ccCurrentUser(revision.getUser(), revision);
     353          23 :         selfAddition.op.suppressEmail();
     354          23 :         selfAddition.op.suppressEvent();
     355          23 :         bu.addOp(revision.getChange().getId(), selfAddition.op);
     356             :       }
     357             : 
     358             :       // Add WorkInProgressOp if requested.
     359          65 :       if ((input.ready || input.workInProgress)
     360           3 :           && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
     361           3 :         if (input.ready && input.workInProgress) {
     362           1 :           output.error = ERROR_WIP_READY_MUTUALLY_EXCLUSIVE;
     363           1 :           return Response.withStatusCode(SC_BAD_REQUEST, output);
     364             :         }
     365             : 
     366           3 :         revision
     367           3 :             .getChangeResource()
     368           3 :             .permissions()
     369           3 :             .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
     370             : 
     371           3 :         if (input.ready) {
     372           3 :           output.ready = true;
     373             :         }
     374             : 
     375           3 :         logger.atFine().log("setting work-in-progress to %s", input.workInProgress);
     376           3 :         WorkInProgressOp wipOp =
     377           3 :             workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input());
     378           3 :         wipOp.suppressEmail();
     379           3 :         bu.addOp(revision.getChange().getId(), wipOp);
     380             :       }
     381             : 
     382             :       // Add the review ops.
     383          65 :       logger.atFine().log("posting review");
     384          65 :       PostReviewOp postReviewOp =
     385          65 :           postReviewOpFactory.create(projectState, revision.getPatchSet().id(), input);
     386          65 :       bu.addOp(revision.getChange().getId(), postReviewOp);
     387          65 :       bu.addOp(
     388          65 :           revision.getChange().getId(),
     389          65 :           postReviewCopyApprovalsOpFactory.create(revision.getPatchSet().id()));
     390             : 
     391             :       // Adjust the attention set based on the input
     392          65 :       replyAttentionSetUpdates.updateAttentionSet(
     393          65 :           bu, revision.getNotes(), input, revision.getUser());
     394          65 :       bu.execute();
     395           1 :     }
     396             : 
     397             :     // Re-read change to take into account results of the update.
     398          65 :     ChangeData cd = changeDataFactory.create(revision.getProject(), revision.getChange().getId());
     399          65 :     for (ReviewerModification reviewerResult : reviewerResults) {
     400          11 :       reviewerResult.gatherResults(cd);
     401          11 :     }
     402             : 
     403             :     // Sending emails and events from ReviewersOps was suppressed so we can send a single batch
     404             :     // email/event here.
     405          65 :     batchEmailReviewers(revision.getUser(), revision.getChange(), reviewerResults, notify);
     406          65 :     batchReviewerEvents(revision.getUser(), cd, revision.getPatchSet(), reviewerResults, ts);
     407             : 
     408          65 :     return Response.ok(output);
     409             :   }
     410             : 
     411             :   private boolean didWorkInProgressChange(boolean currentWorkInProgress, ReviewInput input) {
     412           3 :     return input.ready == currentWorkInProgress || input.workInProgress != currentWorkInProgress;
     413             :   }
     414             : 
     415             :   private NotifyHandling defaultNotify(Change c, ReviewInput in) {
     416          65 :     boolean workInProgress = c.isWorkInProgress();
     417          65 :     if (in.workInProgress) {
     418           3 :       workInProgress = true;
     419             :     }
     420          65 :     if (in.ready) {
     421           3 :       workInProgress = false;
     422             :     }
     423             : 
     424          65 :     if (ChangeMessagesUtil.isAutogenerated(in.tag)) {
     425             :       // Autogenerated comments default to lower notify levels.
     426           5 :       return workInProgress ? NotifyHandling.OWNER : NotifyHandling.OWNER_REVIEWERS;
     427             :     }
     428             : 
     429          64 :     if (workInProgress && !c.hasReviewStarted()) {
     430             :       // If review hasn't started we want to eliminate notifications, no matter who the author is.
     431          13 :       return NotifyHandling.NONE;
     432             :     }
     433             : 
     434             :     // Otherwise, it's either a non-WIP change, or a WIP change where review has started. Notify
     435             :     // everyone.
     436          64 :     return NotifyHandling.ALL;
     437             :   }
     438             : 
     439             :   private void batchEmailReviewers(
     440             :       CurrentUser user,
     441             :       Change change,
     442             :       List<ReviewerModification> reviewerModifications,
     443             :       NotifyResolver.Result notify) {
     444          65 :     try (TraceContext.TraceTimer ignored =
     445          65 :         TraceContext.newTimer(
     446          65 :             getClass().getSimpleName() + "#batchEmailReviewers", Metadata.empty())) {
     447          65 :       List<Account.Id> to = new ArrayList<>();
     448          65 :       List<Account.Id> cc = new ArrayList<>();
     449          65 :       List<Account.Id> removed = new ArrayList<>();
     450          65 :       List<Address> toByEmail = new ArrayList<>();
     451          65 :       List<Address> ccByEmail = new ArrayList<>();
     452          65 :       List<Address> removedByEmail = new ArrayList<>();
     453          65 :       for (ReviewerModification modification : reviewerModifications) {
     454          11 :         Result reviewAdditionResult = modification.op.getResult();
     455          11 :         if (modification.state() == ReviewerState.REVIEWER
     456          11 :             && (!reviewAdditionResult.addedReviewers().isEmpty()
     457           8 :                 || !reviewAdditionResult.addedReviewersByEmail().isEmpty())) {
     458          11 :           to.addAll(modification.reviewers.stream().map(Account::id).collect(toImmutableSet()));
     459          11 :           toByEmail.addAll(modification.reviewersByEmail);
     460          11 :         } else if (modification.state() == ReviewerState.CC
     461          10 :             && (!reviewAdditionResult.addedCCs().isEmpty()
     462           7 :                 || !reviewAdditionResult.addedCCsByEmail().isEmpty())) {
     463          10 :           cc.addAll(modification.reviewers.stream().map(Account::id).collect(toImmutableSet()));
     464          10 :           ccByEmail.addAll(modification.reviewersByEmail);
     465           4 :         } else if (modification.state() == ReviewerState.REMOVED
     466           3 :             && (reviewAdditionResult.deletedReviewer().isPresent()
     467           1 :                 || reviewAdditionResult.deletedReviewerByEmail().isPresent())) {
     468           3 :           reviewAdditionResult.deletedReviewer().ifPresent(d -> removed.add(d));
     469           3 :           reviewAdditionResult.deletedReviewerByEmail().ifPresent(d -> removedByEmail.add(d));
     470             :         }
     471          11 :       }
     472          65 :       modifyReviewersEmail.emailReviewersAsync(
     473          65 :           user.asIdentifiedUser(),
     474             :           change,
     475             :           to,
     476             :           cc,
     477             :           removed,
     478             :           toByEmail,
     479             :           ccByEmail,
     480             :           removedByEmail,
     481             :           notify);
     482             :     }
     483          65 :   }
     484             : 
     485             :   private void batchReviewerEvents(
     486             :       CurrentUser user,
     487             :       ChangeData cd,
     488             :       PatchSet patchSet,
     489             :       List<ReviewerModification> reviewerModifications,
     490             :       Instant when) {
     491          65 :     List<AccountState> newlyAddedReviewers = new ArrayList<>();
     492             : 
     493             :     // There are no events for CCs and reviewers added/deleted by email.
     494          65 :     for (ReviewerModification modification : reviewerModifications) {
     495          11 :       Result reviewerAdditionResult = modification.op.getResult();
     496          11 :       if (modification.state() == ReviewerState.REVIEWER) {
     497          11 :         newlyAddedReviewers.addAll(
     498          11 :             reviewerAdditionResult.addedReviewers().stream()
     499          11 :                 .map(psa -> psa.accountId())
     500          11 :                 .map(accountId -> accountCache.get(accountId))
     501          11 :                 .flatMap(Streams::stream)
     502          11 :                 .collect(toList()));
     503          10 :       } else if (modification.state() == ReviewerState.REMOVED) {
     504             :         // There is no batch event for reviewer removals, hence fire the event for each
     505             :         // modification that deleted a reviewer immediately.
     506           3 :         modification.op.sendEvent();
     507             :       }
     508          11 :     }
     509             : 
     510             :     // Fire a batch event for all newly added reviewers.
     511          65 :     reviewerAdded.fire(cd, patchSet, newlyAddedReviewers, user.asIdentifiedUser().state(), when);
     512          65 :   }
     513             : 
     514             :   private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in)
     515             :       throws BadRequestException, AuthException, UnprocessableEntityException,
     516             :           ResourceConflictException, PermissionBackendException, IOException,
     517             :           ConfigInvalidException {
     518           2 :     logger.atFine().log("request is executed on behalf of %s", in.onBehalfOf);
     519             : 
     520           2 :     if (in.labels == null || in.labels.isEmpty()) {
     521           1 :       throw new AuthException(
     522           1 :           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
     523             :     }
     524           2 :     if (in.drafts != DraftHandling.KEEP) {
     525           1 :       throw new AuthException("not allowed to modify other user's drafts");
     526             :     }
     527             : 
     528           2 :     logger.atFine().log("label input: %s", in.labels);
     529             : 
     530           2 :     CurrentUser caller = rev.getUser();
     531           2 :     PermissionBackend.ForChange perm = rev.permissions();
     532           2 :     Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
     533           2 :     while (itr.hasNext()) {
     534           2 :       Map.Entry<String, Short> ent = itr.next();
     535           2 :       Optional<LabelType> type = labelTypes.byLabel(ent.getKey());
     536           2 :       if (!type.isPresent()) {
     537           1 :         logger.atFine().log("label %s not found", ent.getKey());
     538           1 :         if (strictLabels) {
     539           1 :           throw new BadRequestException(
     540           1 :               String.format("label \"%s\" is not a configured label", ent.getKey()));
     541             :         }
     542           1 :         logger.atFine().log("ignoring input for unknown label %s", ent.getKey());
     543           1 :         itr.remove();
     544           1 :         continue;
     545             :       }
     546             : 
     547           2 :       if (caller.isInternalUser()) {
     548           0 :         logger.atFine().log(
     549             :             "skipping on behalf of permission check for label %s"
     550             :                 + " because caller is an internal user",
     551           0 :             type.get().getName());
     552             :       } else {
     553             :         try {
     554           2 :           perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type.get(), ent.getValue()));
     555           1 :         } catch (AuthException e) {
     556           1 :           throw new AuthException(
     557           1 :               String.format(
     558             :                   "not permitted to modify label \"%s\" on behalf of \"%s\"",
     559           1 :                   type.get().getName(), in.onBehalfOf),
     560             :               e);
     561           2 :         }
     562             :       }
     563           2 :     }
     564           2 :     if (in.labels.isEmpty()) {
     565           0 :       logger.atFine().log("labels are empty after unknown labels have been removed");
     566           0 :       throw new AuthException(
     567           0 :           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
     568             :     }
     569             : 
     570           2 :     IdentifiedUser reviewer = accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
     571           2 :     logger.atFine().log("on behalf of user was resolved to %s", reviewer.getLoggableName());
     572             :     try {
     573           2 :       permissionBackend.user(reviewer).change(rev.getNotes()).check(ChangePermission.READ);
     574           1 :     } catch (AuthException e) {
     575           1 :       throw new ResourceConflictException(
     576           1 :           String.format("on_behalf_of account %s cannot see change", reviewer.getAccountId()), e);
     577           2 :     }
     578             : 
     579           2 :     return new RevisionResource(
     580           2 :         changeResourceFactory.create(rev.getNotes(), reviewer), rev.getPatchSet());
     581             :   }
     582             : 
     583             :   private void checkLabels(RevisionResource rsrc, LabelTypes labelTypes, Map<String, Short> labels)
     584             :       throws BadRequestException, AuthException, PermissionBackendException {
     585          58 :     logger.atFine().log("checking label input: %s", labels);
     586             : 
     587          58 :     PermissionBackend.ForChange perm = rsrc.permissions();
     588          58 :     Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator();
     589          58 :     while (itr.hasNext()) {
     590          58 :       Map.Entry<String, Short> ent = itr.next();
     591          58 :       Optional<LabelType> lt = labelTypes.byLabel(ent.getKey());
     592          58 :       if (!lt.isPresent()) {
     593           2 :         logger.atFine().log("label %s not found", ent.getKey());
     594           2 :         if (strictLabels) {
     595           1 :           throw new BadRequestException(
     596           1 :               String.format("label \"%s\" is not a configured label", ent.getKey()));
     597             :         }
     598           2 :         logger.atFine().log("ignoring input for unknown label %s", ent.getKey());
     599           2 :         itr.remove();
     600           2 :         continue;
     601             :       }
     602             : 
     603          58 :       if (ent.getValue() == null || ent.getValue() == 0) {
     604             :         // Always permit 0, even if it is not within range.
     605             :         // Later null/0 will be deleted and revoke the label.
     606          15 :         continue;
     607             :       }
     608             : 
     609          58 :       if (lt.get().getValue(ent.getValue()) == null) {
     610           1 :         logger.atFine().log("label value %s not found", ent.getValue());
     611           1 :         if (strictLabels) {
     612           1 :           throw new BadRequestException(
     613           1 :               String.format("label \"%s\": %d is not a valid value", ent.getKey(), ent.getValue()));
     614             :         }
     615           1 :         logger.atFine().log(
     616           1 :             "ignoring input for label %s because label value is unknown", ent.getKey());
     617           1 :         itr.remove();
     618           1 :         continue;
     619             :       }
     620             : 
     621          58 :       short val = ent.getValue();
     622             :       try {
     623          58 :         perm.check(new LabelPermission.WithValue(lt.get(), val));
     624           3 :       } catch (AuthException e) {
     625           3 :         throw new AuthException(
     626           3 :             String.format("Applying label \"%s\": %d is restricted", lt.get().getName(), val), e);
     627          58 :       }
     628          58 :     }
     629          58 :   }
     630             : 
     631             :   private static <T extends com.google.gerrit.extensions.client.Comment>
     632             :       Map<String, List<T>> cleanUpComments(Map<String, List<T>> commentsPerPath) {
     633          21 :     Map<String, List<T>> cleanedUpCommentMap = new HashMap<>();
     634          21 :     for (Map.Entry<String, List<T>> e : commentsPerPath.entrySet()) {
     635          21 :       String path = e.getKey();
     636          21 :       List<T> comments = e.getValue();
     637             : 
     638          21 :       if (comments == null) {
     639           0 :         continue;
     640             :       }
     641             : 
     642          21 :       List<T> cleanedUpComments = cleanUpComments(comments);
     643          21 :       if (!cleanedUpComments.isEmpty()) {
     644          20 :         cleanedUpCommentMap.put(path, cleanedUpComments);
     645             :       }
     646          21 :     }
     647          21 :     return cleanedUpCommentMap;
     648             :   }
     649             : 
     650             :   private static <T extends com.google.gerrit.extensions.client.Comment> List<T> cleanUpComments(
     651             :       List<T> comments) {
     652          21 :     return comments.stream()
     653          21 :         .filter(Objects::nonNull)
     654          21 :         .filter(comment -> !Strings.nullToEmpty(comment.message).trim().isEmpty())
     655          21 :         .collect(toList());
     656             :   }
     657             : 
     658             :   private <T extends com.google.gerrit.extensions.client.Comment> void checkComments(
     659             :       RevisionResource revision, Map<String, List<T>> commentsPerPath)
     660             :       throws BadRequestException, PatchListNotAvailableException {
     661          21 :     logger.atFine().log("checking comments");
     662          21 :     Set<String> revisionFilePaths = getAffectedFilePaths(revision);
     663          21 :     for (Map.Entry<String, List<T>> entry : commentsPerPath.entrySet()) {
     664          20 :       String path = entry.getKey();
     665          20 :       PatchSet.Id patchSetId = revision.getPatchSet().id();
     666          20 :       ensurePathRefersToAvailableOrMagicFile(path, revisionFilePaths, patchSetId);
     667             : 
     668          20 :       List<T> comments = entry.getValue();
     669          20 :       for (T comment : comments) {
     670          20 :         ensureLineIsNonNegative(comment.line, path);
     671          20 :         ensureCommentNotOnMagicFilesOfAutoMerge(path, comment);
     672          20 :         ensureRangeIsValid(path, comment.range);
     673          20 :         ensureValidPatchsetLevelComment(path, comment);
     674          20 :         ensureValidInReplyTo(revision.getNotes(), comment.inReplyTo);
     675          20 :       }
     676          20 :     }
     677          21 :   }
     678             : 
     679             :   /**
     680             :    * Asserts that the draft IDs to publish are valid, i.e. they exist and belong to the current
     681             :    * user. If the {@code draftHandling} parameter is equal to {@link DraftHandling#PUBLISH}, then
     682             :    * draft IDs should all correspond to the target revision, otherwise we throw a
     683             :    * BadRequestException.
     684             :    */
     685             :   private void checkDraftIds(
     686             :       RevisionResource resource, List<String> draftIds, DraftHandling draftHandling)
     687             :       throws BadRequestException {
     688           1 :     Map<String, HumanComment> draftsByUuid =
     689           1 :         commentsUtil.draftByChangeAuthor(resource.getNotes(), resource.getUser().getAccountId())
     690           1 :             .stream()
     691           1 :             .collect(Collectors.toMap(c -> c.key.uuid, c -> c));
     692           1 :     List<String> nonExistingDraftIds =
     693           1 :         draftIds.stream().filter(id -> !draftsByUuid.containsKey(id)).collect(toList());
     694           1 :     if (!nonExistingDraftIds.isEmpty()) {
     695           1 :       throw new BadRequestException("Non-existing draft IDs: " + nonExistingDraftIds);
     696             :     }
     697           1 :     if (draftHandling == DraftHandling.PUBLISH_ALL_REVISIONS
     698             :         || draftHandling == DraftHandling.KEEP) {
     699           1 :       return;
     700             :     }
     701           1 :     List<String> draftsForOtherRevisions =
     702           1 :         draftIds.stream()
     703           1 :             .filter(id -> draftsByUuid.get(id).key.patchSetId != resource.getPatchSet().number())
     704           1 :             .collect(toList());
     705           1 :     if (!draftsForOtherRevisions.isEmpty()) {
     706           1 :       throw new BadRequestException(
     707           1 :           String.format(
     708             :               "Draft comments for other revisions cannot be published when DraftHandling = PUBLISH."
     709             :                   + " (draft IDs: %s)",
     710             :               draftsForOtherRevisions));
     711             :     }
     712           0 :   }
     713             : 
     714             :   private Set<String> getAffectedFilePaths(RevisionResource revision)
     715             :       throws PatchListNotAvailableException {
     716          21 :     ObjectId newId = revision.getPatchSet().commitId();
     717          21 :     DiffSummaryKey key =
     718          21 :         DiffSummaryKey.fromPatchListKey(
     719          21 :             PatchListKey.againstDefaultBase(newId, Whitespace.IGNORE_NONE));
     720          21 :     DiffSummary ds = patchListCache.getDiffSummary(key, revision.getProject());
     721          21 :     return new HashSet<>(ds.getPaths());
     722             :   }
     723             : 
     724             :   private static void ensurePathRefersToAvailableOrMagicFile(
     725             :       String path, Set<String> availableFilePaths, PatchSet.Id patchSetId)
     726             :       throws BadRequestException {
     727          20 :     if (!availableFilePaths.contains(path) && !Patch.isMagic(path)) {
     728           1 :       throw new BadRequestException(
     729           1 :           String.format("file %s not found in revision %s", path, patchSetId));
     730             :     }
     731          20 :   }
     732             : 
     733             :   private static void ensureLineIsNonNegative(Integer line, String path)
     734             :       throws BadRequestException {
     735          20 :     if (line != null && line < 0) {
     736           0 :       throw new BadRequestException(
     737           0 :           String.format("negative line number %d not allowed on %s", line, path));
     738             :     }
     739          20 :   }
     740             : 
     741             :   private static <T extends com.google.gerrit.extensions.client.Comment>
     742             :       void ensureCommentNotOnMagicFilesOfAutoMerge(String path, T comment)
     743             :           throws BadRequestException {
     744          20 :     if (Patch.isMagic(path) && comment.side == Side.PARENT && comment.parent == null) {
     745           1 :       throw new BadRequestException(String.format("cannot comment on %s on auto-merge", path));
     746             :     }
     747          20 :   }
     748             : 
     749             :   private static <T extends com.google.gerrit.extensions.client.Comment>
     750             :       void ensureValidPatchsetLevelComment(String path, T comment) throws BadRequestException {
     751          20 :     if (path.equals(PATCHSET_LEVEL)
     752             :         && (comment.side != null || comment.range != null || comment.line != null)) {
     753           2 :       throw new BadRequestException("Patchset-level comments can't have side, range, or line");
     754             :     }
     755          20 :   }
     756             : 
     757             :   private void ensureValidInReplyTo(ChangeNotes changeNotes, String inReplyTo)
     758             :       throws BadRequestException {
     759          20 :     if (inReplyTo != null
     760           3 :         && !commentsUtil.getPublishedHumanComment(changeNotes, inReplyTo).isPresent()
     761           3 :         && !commentsUtil.getRobotComment(changeNotes, inReplyTo).isPresent()) {
     762           2 :       throw new BadRequestException(
     763           2 :           String.format("Invalid inReplyTo, comment %s not found", inReplyTo));
     764             :     }
     765          20 :   }
     766             : 
     767             :   private void checkRobotComments(
     768             :       RevisionResource revision, Map<String, List<RobotCommentInput>> in)
     769             :       throws BadRequestException, PatchListNotAvailableException {
     770           7 :     logger.atFine().log("checking robot comments");
     771           7 :     for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) {
     772           7 :       String commentPath = e.getKey();
     773           7 :       for (RobotCommentInput c : e.getValue()) {
     774           7 :         ensureRobotIdIsSet(c.robotId, commentPath);
     775           7 :         ensureRobotRunIdIsSet(c.robotRunId, commentPath);
     776           7 :         ensureFixSuggestionsAreAddable(c.fixSuggestions, commentPath);
     777             :         // Size is validated later, in CommentLimitsValidator.
     778           7 :       }
     779           7 :     }
     780           7 :     checkComments(revision, in);
     781           7 :   }
     782             : 
     783             :   private static void ensureRobotIdIsSet(String robotId, String commentPath)
     784             :       throws BadRequestException {
     785           7 :     if (robotId == null) {
     786           0 :       throw new BadRequestException(
     787           0 :           String.format("robotId is missing for robot comment on %s", commentPath));
     788             :     }
     789           7 :   }
     790             : 
     791             :   private static void ensureRobotRunIdIsSet(String robotRunId, String commentPath)
     792             :       throws BadRequestException {
     793           7 :     if (robotRunId == null) {
     794           0 :       throw new BadRequestException(
     795           0 :           String.format("robotRunId is missing for robot comment on %s", commentPath));
     796             :     }
     797           7 :   }
     798             : 
     799             :   private static void ensureFixSuggestionsAreAddable(
     800             :       List<FixSuggestionInfo> fixSuggestionInfos, String commentPath) throws BadRequestException {
     801           7 :     if (fixSuggestionInfos == null) {
     802           6 :       return;
     803             :     }
     804             : 
     805           3 :     for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
     806           2 :       ensureDescriptionIsSet(commentPath, fixSuggestionInfo.description);
     807           2 :       ensureFixReplacementsAreAddable(commentPath, fixSuggestionInfo.replacements);
     808           2 :     }
     809           3 :   }
     810             : 
     811             :   private static void ensureDescriptionIsSet(String commentPath, String description)
     812             :       throws BadRequestException {
     813           2 :     if (description == null) {
     814           1 :       throw new BadRequestException(
     815           1 :           String.format(
     816             :               "A description is required for the suggested fix of the robot comment on %s",
     817             :               commentPath));
     818             :     }
     819           2 :   }
     820             : 
     821             :   private static void ensureFixReplacementsAreAddable(
     822             :       String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
     823           2 :     ensureReplacementsArePresent(commentPath, fixReplacementInfos);
     824             : 
     825           2 :     for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) {
     826           2 :       ensureReplacementPathIsSetAndNotPatchsetLevel(commentPath, fixReplacementInfo.path);
     827           2 :       ensureRangeIsSet(commentPath, fixReplacementInfo.range);
     828           2 :       ensureRangeIsValid(commentPath, fixReplacementInfo.range);
     829           2 :       ensureReplacementStringIsSet(commentPath, fixReplacementInfo.replacement);
     830           2 :     }
     831             : 
     832           2 :     Map<String, List<FixReplacementInfo>> replacementsPerFilePath =
     833           2 :         fixReplacementInfos.stream().collect(groupingBy(fixReplacement -> fixReplacement.path));
     834           2 :     for (List<FixReplacementInfo> sameFileReplacements : replacementsPerFilePath.values()) {
     835           2 :       ensureRangesDoNotOverlap(commentPath, sameFileReplacements);
     836           2 :     }
     837           2 :   }
     838             : 
     839             :   private static void ensureReplacementsArePresent(
     840             :       String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
     841           2 :     if (fixReplacementInfos == null || fixReplacementInfos.isEmpty()) {
     842           1 :       throw new BadRequestException(
     843           1 :           String.format(
     844             :               "At least one replacement is "
     845             :                   + "required for the suggested fix of the robot comment on %s",
     846             :               commentPath));
     847             :     }
     848           2 :   }
     849             : 
     850             :   private static void ensureReplacementPathIsSetAndNotPatchsetLevel(
     851             :       String commentPath, String replacementPath) throws BadRequestException {
     852           2 :     if (replacementPath == null) {
     853           1 :       throw new BadRequestException(
     854           1 :           String.format(
     855             :               "A file path must be given for the replacement of the robot comment on %s",
     856             :               commentPath));
     857             :     }
     858           2 :     if (replacementPath.equals(PATCHSET_LEVEL)) {
     859           1 :       throw new BadRequestException(
     860           1 :           String.format(
     861             :               "A file path must not be %s for the replacement of the robot comment on %s",
     862             :               PATCHSET_LEVEL, commentPath));
     863             :     }
     864           2 :   }
     865             : 
     866             :   private static void ensureRangeIsSet(String commentPath, Range range) throws BadRequestException {
     867           2 :     if (range == null) {
     868           1 :       throw new BadRequestException(
     869           1 :           String.format(
     870             :               "A range must be given for the replacement of the robot comment on %s", commentPath));
     871             :     }
     872           2 :   }
     873             : 
     874             :   private static void ensureRangeIsValid(String commentPath, Range range)
     875             :       throws BadRequestException {
     876          20 :     if (range == null) {
     877          20 :       return;
     878             :     }
     879           7 :     if (!range.isValid()) {
     880           2 :       throw new BadRequestException(
     881           2 :           String.format(
     882             :               "Range (%s:%s - %s:%s) is not valid for the comment on %s",
     883           2 :               range.startLine,
     884           2 :               range.startCharacter,
     885           2 :               range.endLine,
     886           2 :               range.endCharacter,
     887             :               commentPath));
     888             :     }
     889           6 :   }
     890             : 
     891             :   private static void ensureReplacementStringIsSet(String commentPath, String replacement)
     892             :       throws BadRequestException {
     893           2 :     if (replacement == null) {
     894           1 :       throw new BadRequestException(
     895           1 :           String.format(
     896             :               "A content for replacement "
     897             :                   + "must be indicated for the replacement of the robot comment on %s",
     898             :               commentPath));
     899             :     }
     900           2 :   }
     901             : 
     902             :   private static void ensureRangesDoNotOverlap(
     903             :       String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
     904           2 :     List<Range> sortedRanges =
     905           2 :         fixReplacementInfos.stream()
     906           2 :             .map(fixReplacementInfo -> fixReplacementInfo.range)
     907           2 :             .sorted()
     908           2 :             .collect(toList());
     909             : 
     910           2 :     int previousEndLine = 0;
     911           2 :     int previousOffset = -1;
     912           2 :     for (Range range : sortedRanges) {
     913           2 :       if (range.startLine < previousEndLine
     914             :           || (range.startLine == previousEndLine && range.startCharacter < previousOffset)) {
     915           1 :         throw new BadRequestException(
     916           1 :             String.format("Replacements overlap for the robot comment on %s", commentPath));
     917             :       }
     918           2 :       previousEndLine = range.endLine;
     919           2 :       previousOffset = range.endCharacter;
     920           2 :     }
     921           2 :   }
     922             : 
     923             :   /**
     924             :    * Used to compare existing {@link HumanComment}-s with {@link CommentInput} comments by copying
     925             :    * only the fields to compare.
     926             :    */
     927             :   @AutoValue
     928          20 :   abstract static class CommentSetEntry {
     929             :     private static CommentSetEntry create(
     930             :         String filename,
     931             :         int patchSetId,
     932             :         Integer line,
     933             :         Side side,
     934             :         HashCode message,
     935             :         Comment.Range range) {
     936          20 :       return new AutoValue_PostReview_CommentSetEntry(
     937             :           filename, patchSetId, line, side, message, range);
     938             :     }
     939             : 
     940             :     public static CommentSetEntry create(Comment comment) {
     941          20 :       return create(
     942             :           comment.key.filename,
     943             :           comment.key.patchSetId,
     944          20 :           comment.lineNbr,
     945          20 :           Side.fromShort(comment.side),
     946          20 :           Hashing.murmur3_128().hashString(comment.message, UTF_8),
     947             :           comment.range);
     948             :     }
     949             : 
     950             :     abstract String filename();
     951             : 
     952             :     abstract int patchSetId();
     953             : 
     954             :     @Nullable
     955             :     abstract Integer line();
     956             : 
     957             :     abstract Side side();
     958             : 
     959             :     abstract HashCode message();
     960             : 
     961             :     @Nullable
     962             :     abstract Comment.Range range();
     963             :   }
     964             : }

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