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

com.google.gerrit.server.restapi.change.PostReview Maven / Gradle / Ivy

There is a newer version: 3.10.1
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.restapi.change;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;

import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.api.changes.ReviewerResult;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.metrics.Counter1;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.CommentsValidator;
import com.google.gerrit.server.change.ModifyReviewersEmail;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerModifier;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
import com.google.gerrit.server.change.ReviewerOp.Result;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.ReviewerAdded;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdates;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;

@Singleton
public class PostReview implements RestModifyView {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  @Singleton
  private static class Metrics {
    final Counter1 draftHandling;

    @Inject
    Metrics(MetricMaker metricMaker) {
      draftHandling =
          metricMaker.newCounter(
              "change/post_review/draft_handling",
              new Description(
                      "Total number of draft handling option "
                          + "(KEEP, PUBLISH, PUBLISH_ALL_REVISIONS) "
                          + "selected by users while posting a review.")
                  .setRate()
                  .setUnit("count"),
              Field.ofString("type", Metadata.Builder::eventType)
                  .description(
                      "The type of the draft handling option"
                          + " (KEEP, PUBLISH, PUBLISH_ALL_REVISIONS).")
                  .build());
    }
  }

  private static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
  public static final String ERROR_WIP_READY_MUTUALLY_EXCLUSIVE =
      "work_in_progress and ready are mutually exclusive";

  private final RetryHelper retryHelper;
  private final PostReviewOp.Factory postReviewOpFactory;
  private final ChangeResource.Factory changeResourceFactory;
  private final AccountCache accountCache;
  private final ApprovalsUtil approvalsUtil;
  private final DraftCommentsReader draftCommentsReader;

  private final AccountResolver accountResolver;
  private final ReviewerModifier reviewerModifier;
  private final Metrics metrics;
  private final ModifyReviewersEmail modifyReviewersEmail;
  private final NotifyResolver notifyResolver;
  private final WorkInProgressOp.Factory workInProgressOpFactory;
  private final ProjectCache projectCache;
  private final PermissionBackend permissionBackend;

  private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
  private final ReviewerAdded reviewerAdded;
  private final boolean strictLabels;
  private final ChangeJson.Factory changeJsonFactory;
  private final CommentsValidator commentsValidator;

  @Inject
  PostReview(
      RetryHelper retryHelper,
      PostReviewOp.Factory postReviewOpFactory,
      ChangeResource.Factory changeResourceFactory,
      AccountCache accountCache,
      ApprovalsUtil approvalsUtil,
      DraftCommentsReader draftCommentsReader,
      AccountResolver accountResolver,
      ReviewerModifier reviewerModifier,
      Metrics metrics,
      ModifyReviewersEmail modifyReviewersEmail,
      NotifyResolver notifyResolver,
      @GerritServerConfig Config gerritConfig,
      WorkInProgressOp.Factory workInProgressOpFactory,
      ProjectCache projectCache,
      PermissionBackend permissionBackend,
      ReplyAttentionSetUpdates replyAttentionSetUpdates,
      ReviewerAdded reviewerAdded,
      ChangeJson.Factory changeJsonFactory,
      CommentsValidator commentsValidator) {
    this.retryHelper = retryHelper;
    this.postReviewOpFactory = postReviewOpFactory;
    this.changeResourceFactory = changeResourceFactory;
    this.accountCache = accountCache;
    this.draftCommentsReader = draftCommentsReader;
    this.approvalsUtil = approvalsUtil;
    this.accountResolver = accountResolver;
    this.reviewerModifier = reviewerModifier;
    this.metrics = metrics;
    this.modifyReviewersEmail = modifyReviewersEmail;
    this.notifyResolver = notifyResolver;
    this.workInProgressOpFactory = workInProgressOpFactory;
    this.projectCache = projectCache;
    this.permissionBackend = permissionBackend;
    this.replyAttentionSetUpdates = replyAttentionSetUpdates;
    this.reviewerAdded = reviewerAdded;
    this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
    this.changeJsonFactory = changeJsonFactory;
    this.commentsValidator = commentsValidator;
  }

  @Override
  public Response apply(RevisionResource revision, ReviewInput input)
      throws RestApiException, UpdateException, IOException, PermissionBackendException,
          ConfigInvalidException, PatchListNotAvailableException {
    return apply(revision, input, TimeUtil.now());
  }

  public Response apply(RevisionResource revision, ReviewInput input, Instant ts)
      throws RestApiException, UpdateException, IOException, PermissionBackendException,
          ConfigInvalidException, PatchListNotAvailableException {
    // Respect timestamp, but truncate at change created-on time.
    ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
    if (revision.getEdit().isPresent()) {
      throw new ResourceConflictException("cannot post review on edit");
    }
    ProjectState projectState =
        projectCache.get(revision.getProject()).orElseThrow(illegalState(revision.getProject()));
    LabelTypes labelTypes = projectState.getLabelTypes(revision.getNotes());

    logger.atFine().log("strict label checking is %s", (strictLabels ? "enabled" : "disabled"));

    metrics.draftHandling.increment(input.drafts == null ? "N/A" : input.drafts.name());
    input.drafts = firstNonNull(input.drafts, DraftHandling.KEEP);
    logger.atFine().log("draft handling = %s", input.drafts);

    if (input.onBehalfOf != null) {
      revision = onBehalfOf(revision, labelTypes, input);
    }
    if (input.labels != null) {
      checkLabels(revision, labelTypes, input.labels);
    }
    if (input.comments != null) {
      input.comments = cleanUpComments(input.comments);
      commentsValidator.checkComments(revision, input.comments);
    }
    if (input.draftIdsToPublish != null) {
      checkDraftIds(revision, input.draftIdsToPublish, input.drafts);
    }
    if (input.robotComments != null) {
      input.robotComments = cleanUpComments(input.robotComments);
      checkRobotComments(revision, input.robotComments);
    }

    if (input.notify == null) {
      input.notify = defaultNotify(revision.getChange(), input);
    }
    logger.atFine().log("notify handling = %s", input.notify);

    Map reviewerJsonResults = null;
    List reviewerResults = Lists.newArrayList();
    boolean hasError = false;
    boolean confirm = false;
    if (input.reviewers != null) {
      reviewerJsonResults = Maps.newHashMap();
      for (ReviewerInput reviewerInput : input.reviewers) {
        ReviewerModification result =
            reviewerModifier.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true);
        reviewerJsonResults.put(reviewerInput.reviewer, result.result);
        if (result.result.error != null) {
          logger.atFine().log(
              "Adding %s as reviewer failed: %s", reviewerInput.reviewer, result.result.error);
          hasError = true;
          continue;
        }
        if (result.result.confirm != null) {
          logger.atFine().log(
              "Adding %s as reviewer requires confirmation", reviewerInput.reviewer);
          confirm = true;
          continue;
        }
        logger.atFine().log("Adding %s as reviewer was prepared", reviewerInput.reviewer);
        reviewerResults.add(result);
      }
    }

    ReviewResult output = new ReviewResult();
    output.reviewers = reviewerJsonResults;
    if (hasError || confirm) {
      output.error = ERROR_ADDING_REVIEWER;
      return Response.withStatusCode(SC_BAD_REQUEST, output);
    }
    output.labels = input.labels;

    Account account = revision.getUser().asIdentifiedUser().getAccount();
    boolean ccOrReviewer = false;
    if (input.labels != null && !input.labels.isEmpty()) {
      ccOrReviewer = input.labels.values().stream().anyMatch(v -> v != 0);
      if (ccOrReviewer) {
        logger.atFine().log("calling user is cc/reviewer on the change due to voting on a label");
      }
    }

    if (!ccOrReviewer) {
      // Check if user was already CCed or reviewing prior to this review.
      ReviewerSet currentReviewers =
          approvalsUtil.getReviewers(revision.getChangeResource().getNotes());
      ccOrReviewer = currentReviewers.all().contains(account.id());
      if (ccOrReviewer) {
        logger.atFine().log("calling user is already cc/reviewer on the change");
      }
    }

    for (ReviewerModification reviewerResult : reviewerResults) {
      reviewerResult.op.suppressEmail(); // Send a single batch email below.
      reviewerResult.op.suppressEvent(); // Send events below, if possible as batch.
      if (!ccOrReviewer && reviewerResult.reviewers.contains(account)) {
        logger.atFine().log("calling user is explicitly added as reviewer or CC");
        ccOrReviewer = true;
      }
    }

    // Notify based on ReviewInput, ignoring the notify settings from any ReviewerInputs.
    NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);

    if ((input.ready || input.workInProgress)
        && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
      if (input.ready && input.workInProgress) {
        output.error = ERROR_WIP_READY_MUTUALLY_EXCLUSIVE;
        return Response.withStatusCode(SC_BAD_REQUEST, output);
      }

      revision
          .getChangeResource()
          .permissions()
          .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);

      if (input.ready) {
        output.ready = true;
      }
    }

    BatchUpdates.Result batchUpdateResult =
        runBatchUpdate(projectState, revision, input, ts, notify, reviewerResults, ccOrReviewer);
    ChangeData cd =
        batchUpdateResult.getChangeData(revision.getProject(), revision.getChange().getId());
    for (ReviewerModification reviewerResult : reviewerResults) {
      reviewerResult.gatherResults(cd);
    }

    // Sending emails and events from ReviewersOps was suppressed so we can send a single batch
    // email/event here.
    batchEmailReviewers(revision.getUser(), revision.getChange(), reviewerResults, notify);
    batchReviewerEvents(revision.getUser(), cd, revision.getPatchSet(), reviewerResults, ts);

    if (input.responseFormatOptions != null) {
      output.changeInfo = changeJsonFactory.create(input.responseFormatOptions).format(cd);
    } else {
      output.changeInfo = changeJsonFactory.noOptions().format(cd);
    }

    return Response.ok(output);
  }

  private BatchUpdates.Result runBatchUpdate(
      ProjectState projectState,
      RevisionResource revision,
      ReviewInput input,
      Instant ts,
      NotifyResolver.Result notify,
      List reviewerResults,
      boolean ccOrReviewer)
      throws UpdateException, RestApiException {
    return retryHelper
        .changeUpdate(
            "batchUpdate",
            updateFactory -> {
              try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
                try (BatchUpdate bu =
                    updateFactory.create(
                        revision.getChange().getProject(), revision.getUser(), ts)) {
                  bu.setNotify(notify);

                  // Apply reviewer changes first. Revision emails should be sent to the
                  // updated set of reviewers. Also keep track of whether the user added
                  // themselves as a reviewer or to the CC list.
                  logger.atFine().log("adding reviewer additions");
                  reviewerResults.forEach(
                      reviewerResult -> bu.addOp(revision.getChange().getId(), reviewerResult.op));

                  if (!ccOrReviewer) {
                    // User posting this review isn't currently in the reviewer or CC list,
                    // isn't being explicitly added, and isn't voting on any label.
                    // Automatically CC them on this change so they receive replies.
                    logger.atFine().log("CCing calling user");
                    ReviewerModification selfAddition =
                        reviewerModifier.ccCurrentUser(revision.getUser(), revision);
                    selfAddition.op.suppressEmail();
                    selfAddition.op.suppressEvent();
                    bu.addOp(revision.getChange().getId(), selfAddition.op);
                  }

                  // Add WorkInProgressOp if requested.
                  if ((input.ready || input.workInProgress)
                      && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
                    logger.atFine().log("setting work-in-progress to %s", input.workInProgress);
                    WorkInProgressOp wipOp =
                        workInProgressOpFactory.create(
                            input.workInProgress, new WorkInProgressOp.Input());
                    wipOp.suppressEmail();
                    bu.addOp(revision.getChange().getId(), wipOp);
                  }

                  // Add the review ops.
                  logger.atFine().log("posting review");
                  PostReviewOp postReviewOp =
                      postReviewOpFactory.create(
                          projectState,
                          revision.getPatchSet().id(),
                          input,
                          revision.getAccountId());
                  bu.addOp(revision.getChange().getId(), postReviewOp);

                  // Adjust the attention set based on the input
                  replyAttentionSetUpdates.updateAttentionSetOnPostReview(
                      bu, postReviewOp, revision.getNotes(), input, revision.getUser());

                  return bu.execute();
                }
              }
            })
        .call();
  }

  private boolean didWorkInProgressChange(boolean currentWorkInProgress, ReviewInput input) {
    return input.ready == currentWorkInProgress || input.workInProgress != currentWorkInProgress;
  }

  private NotifyHandling defaultNotify(Change c, ReviewInput in) {
    boolean workInProgress = c.isWorkInProgress();
    if (in.workInProgress) {
      workInProgress = true;
    }
    if (in.ready) {
      workInProgress = false;
    }

    if (ChangeMessagesUtil.isAutogenerated(in.tag)) {
      // Autogenerated comments default to lower notify levels.
      return workInProgress ? NotifyHandling.OWNER : NotifyHandling.OWNER_REVIEWERS;
    }

    if (workInProgress && !c.hasReviewStarted()) {
      // If review hasn't started we want to eliminate notifications, no matter who the author is.
      return NotifyHandling.NONE;
    }

    // Otherwise, it's either a non-WIP change, or a WIP change where review has started. Notify
    // everyone.
    return NotifyHandling.ALL;
  }

  private void batchEmailReviewers(
      CurrentUser user,
      Change change,
      List reviewerModifications,
      NotifyResolver.Result notify) {
    try (TraceContext.TraceTimer ignored =
        TraceContext.newTimer(
            getClass().getSimpleName() + "#batchEmailReviewers", Metadata.empty())) {
      List to = new ArrayList<>();
      List cc = new ArrayList<>();
      List removed = new ArrayList<>();
      List
toByEmail = new ArrayList<>(); List
ccByEmail = new ArrayList<>(); List
removedByEmail = new ArrayList<>(); for (ReviewerModification modification : reviewerModifications) { Result reviewAdditionResult = modification.op.getResult(); if (modification.state() == ReviewerState.REVIEWER && (!reviewAdditionResult.addedReviewers().isEmpty() || !reviewAdditionResult.addedReviewersByEmail().isEmpty())) { to.addAll(modification.reviewers.stream().map(Account::id).collect(toImmutableSet())); toByEmail.addAll(modification.reviewersByEmail); } else if (modification.state() == ReviewerState.CC && (!reviewAdditionResult.addedCCs().isEmpty() || !reviewAdditionResult.addedCCsByEmail().isEmpty())) { cc.addAll(modification.reviewers.stream().map(Account::id).collect(toImmutableSet())); ccByEmail.addAll(modification.reviewersByEmail); } else if (modification.state() == ReviewerState.REMOVED && (reviewAdditionResult.deletedReviewer().isPresent() || reviewAdditionResult.deletedReviewerByEmail().isPresent())) { reviewAdditionResult.deletedReviewer().ifPresent(d -> removed.add(d)); reviewAdditionResult.deletedReviewerByEmail().ifPresent(d -> removedByEmail.add(d)); } } modifyReviewersEmail.emailReviewersAsync( user.asIdentifiedUser(), change, to, cc, removed, toByEmail, ccByEmail, removedByEmail, notify); } } private void batchReviewerEvents( CurrentUser user, ChangeData cd, PatchSet patchSet, List reviewerModifications, Instant when) { List newlyAddedReviewers = new ArrayList<>(); // There are no events for CCs and reviewers added/deleted by email. for (ReviewerModification modification : reviewerModifications) { Result reviewerAdditionResult = modification.op.getResult(); if (modification.state() == ReviewerState.REVIEWER) { newlyAddedReviewers.addAll( reviewerAdditionResult.addedReviewers().stream() .map(psa -> psa.accountId()) .map(accountId -> accountCache.get(accountId)) .flatMap(Streams::stream) .collect(toList())); } else if (modification.state() == ReviewerState.REMOVED) { // There is no batch event for reviewer removals, hence fire the event for each // modification that deleted a reviewer immediately. modification.op.sendEvent(); } } // Fire a batch event for all newly added reviewers. reviewerAdded.fire(cd, patchSet, newlyAddedReviewers, user.asIdentifiedUser().state(), when); } private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in) throws BadRequestException, AuthException, UnprocessableEntityException, ResourceConflictException, PermissionBackendException, IOException, ConfigInvalidException { logger.atFine().log("request is executed on behalf of %s", in.onBehalfOf); if (in.labels == null || in.labels.isEmpty()) { throw new AuthException( String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf)); } if (in.drafts != DraftHandling.KEEP) { throw new AuthException("not allowed to modify other user's drafts"); } logger.atFine().log("label input: %s", in.labels); CurrentUser caller = rev.getUser(); PermissionBackend.ForChange perm = rev.permissions(); Iterator> itr = in.labels.entrySet().iterator(); while (itr.hasNext()) { Map.Entry ent = itr.next(); Optional type = labelTypes.byLabel(ent.getKey()); if (!type.isPresent()) { logger.atFine().log("label %s not found", ent.getKey()); if (strictLabels) { throw new BadRequestException( String.format("label \"%s\" is not a configured label", ent.getKey())); } logger.atFine().log("ignoring input for unknown label %s", ent.getKey()); itr.remove(); continue; } if (caller.isInternalUser()) { logger.atFine().log( "skipping on behalf of permission check for label %s" + " because caller is an internal user", type.get().getName()); } else { try { perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type.get(), ent.getValue())); } catch (AuthException e) { throw new AuthException( String.format( "not permitted to modify label \"%s\" on behalf of \"%s\"", type.get().getName(), in.onBehalfOf), e); } } } if (in.labels.isEmpty()) { logger.atFine().log("labels are empty after unknown labels have been removed"); throw new AuthException( String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf)); } IdentifiedUser reviewer = accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller); logger.atFine().log("on behalf of user was resolved to %s", reviewer.getLoggableName()); try { permissionBackend.user(reviewer).change(rev.getNotes()).check(ChangePermission.READ); } catch (AuthException e) { throw new ResourceConflictException( String.format("on_behalf_of account %s cannot see change", reviewer.getAccountId()), e); } return new RevisionResource( changeResourceFactory.create(rev.getNotes(), reviewer), rev.getPatchSet()); } private void checkLabels(RevisionResource rsrc, LabelTypes labelTypes, Map labels) throws BadRequestException, AuthException, PermissionBackendException { logger.atFine().log("checking label input: %s", labels); PermissionBackend.ForChange perm = rsrc.permissions(); Iterator> itr = labels.entrySet().iterator(); while (itr.hasNext()) { Map.Entry ent = itr.next(); Optional lt = labelTypes.byLabel(ent.getKey()); if (!lt.isPresent()) { logger.atFine().log("label %s not found", ent.getKey()); if (strictLabels) { throw new BadRequestException( String.format("label \"%s\" is not a configured label", ent.getKey())); } logger.atFine().log("ignoring input for unknown label %s", ent.getKey()); itr.remove(); continue; } if (ent.getValue() == null || ent.getValue() == 0) { // Always permit 0, even if it is not within range. // Later null/0 will be deleted and revoke the label. continue; } if (lt.get().getValue(ent.getValue()) == null) { logger.atFine().log("label value %s not found", ent.getValue()); if (strictLabels) { throw new BadRequestException( String.format("label \"%s\": %d is not a valid value", ent.getKey(), ent.getValue())); } logger.atFine().log( "ignoring input for label %s because label value is unknown", ent.getKey()); itr.remove(); continue; } short val = ent.getValue(); try { perm.check(new LabelPermission.WithValue(lt.get(), val)); } catch (AuthException e) { throw new AuthException( String.format("Applying label \"%s\": %d is restricted", lt.get().getName(), val), e); } } } private static Map> cleanUpComments(Map> commentsPerPath) { Map> cleanedUpCommentMap = new HashMap<>(); for (Map.Entry> e : commentsPerPath.entrySet()) { String path = e.getKey(); List comments = e.getValue(); if (comments == null) { continue; } List cleanedUpComments = cleanUpComments(comments); if (!cleanedUpComments.isEmpty()) { cleanedUpCommentMap.put(path, cleanedUpComments); } } return cleanedUpCommentMap; } private static List cleanUpComments( List comments) { return comments.stream() .filter(Objects::nonNull) .filter(comment -> !Strings.nullToEmpty(comment.message).trim().isEmpty()) .collect(toList()); } /** * Asserts that the draft IDs to publish are valid, i.e. they exist and belong to the current * user. If the {@code draftHandling} parameter is equal to {@link DraftHandling#PUBLISH}, then * draft IDs should all correspond to the target revision, otherwise we throw a * BadRequestException. */ private void checkDraftIds( RevisionResource resource, List draftIds, DraftHandling draftHandling) throws BadRequestException { Map draftsByUuid = draftCommentsReader .getDraftsByChangeAndDraftAuthor(resource.getNotes(), resource.getUser().getAccountId()) .stream() .collect(Collectors.toMap(c -> c.key.uuid, c -> c)); List nonExistingDraftIds = draftIds.stream().filter(id -> !draftsByUuid.containsKey(id)).collect(toList()); if (!nonExistingDraftIds.isEmpty()) { throw new BadRequestException("Non-existing draft IDs: " + nonExistingDraftIds); } if (draftHandling == DraftHandling.PUBLISH_ALL_REVISIONS || draftHandling == DraftHandling.KEEP) { return; } List draftsForOtherRevisions = draftIds.stream() .filter(id -> draftsByUuid.get(id).key.patchSetId != resource.getPatchSet().number()) .collect(toList()); if (!draftsForOtherRevisions.isEmpty()) { throw new BadRequestException( String.format( "Draft comments for other revisions cannot be published when DraftHandling = PUBLISH." + " (draft IDs: %s)", draftsForOtherRevisions)); } } private void checkRobotComments( RevisionResource revision, Map> in) throws BadRequestException, PatchListNotAvailableException { logger.atFine().log("checking robot comments"); for (Map.Entry> e : in.entrySet()) { String commentPath = e.getKey(); for (RobotCommentInput c : e.getValue()) { ensureRobotIdIsSet(c.robotId, commentPath); ensureRobotRunIdIsSet(c.robotRunId, commentPath); // Size is validated later, in CommentLimitsValidator. } } commentsValidator.checkComments(revision, in); } private static void ensureRobotIdIsSet(String robotId, String commentPath) throws BadRequestException { if (robotId == null) { throw new BadRequestException( String.format("robotId is missing for comment on %s", commentPath)); } } private static void ensureRobotRunIdIsSet(String robotRunId, String commentPath) throws BadRequestException { if (robotRunId == null) { throw new BadRequestException( String.format("robotRunId is missing for comment on %s", commentPath)); } } /** * Used to compare existing {@link HumanComment}-s with {@link CommentInput} comments by copying * only the fields to compare. */ @AutoValue abstract static class CommentSetEntry { private static CommentSetEntry create( String filename, int patchSetId, Integer line, Side side, HashCode message, Comment.Range range) { return new AutoValue_PostReview_CommentSetEntry( filename, patchSetId, line, side, message, range); } public static CommentSetEntry create(Comment comment) { return create( comment.key.filename, comment.key.patchSetId, comment.lineNbr, Side.fromShort(comment.side), Hashing.murmur3_128().hashString(comment.message, UTF_8), comment.range); } abstract String filename(); abstract int patchSetId(); @Nullable abstract Integer line(); abstract Side side(); abstract HashCode message(); @Nullable abstract Comment.Range range(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy