LCOV - code coverage report
Current view: top level - server/change - PatchSetInserter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 174 179 97.2 %
Date: 2022-11-19 15:00:39 Functions: 22 22 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2013 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.change;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
      19             : import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
      20             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      21             : import static java.util.Objects.requireNonNull;
      22             : 
      23             : import com.google.common.collect.ImmutableListMultimap;
      24             : import com.google.gerrit.common.Nullable;
      25             : import com.google.gerrit.entities.Change;
      26             : import com.google.gerrit.entities.PatchSet;
      27             : import com.google.gerrit.entities.PatchSetInfo;
      28             : import com.google.gerrit.extensions.api.changes.NotifyHandling;
      29             : import com.google.gerrit.extensions.client.ChangeKind;
      30             : import com.google.gerrit.extensions.restapi.AuthException;
      31             : import com.google.gerrit.extensions.restapi.BadRequestException;
      32             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      33             : import com.google.gerrit.server.ChangeMessagesUtil;
      34             : import com.google.gerrit.server.ChangeUtil;
      35             : import com.google.gerrit.server.PatchSetUtil;
      36             : import com.google.gerrit.server.ReviewerSet;
      37             : import com.google.gerrit.server.approval.ApprovalCopier;
      38             : import com.google.gerrit.server.approval.ApprovalsUtil;
      39             : import com.google.gerrit.server.events.CommitReceivedEvent;
      40             : import com.google.gerrit.server.extensions.events.RevisionCreated;
      41             : import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
      42             : import com.google.gerrit.server.git.validators.CommitValidationException;
      43             : import com.google.gerrit.server.git.validators.CommitValidators;
      44             : import com.google.gerrit.server.notedb.ChangeNotes;
      45             : import com.google.gerrit.server.notedb.ChangeUpdate;
      46             : import com.google.gerrit.server.patch.AutoMerger;
      47             : import com.google.gerrit.server.patch.PatchSetInfoFactory;
      48             : import com.google.gerrit.server.permissions.ChangePermission;
      49             : import com.google.gerrit.server.permissions.PermissionBackend;
      50             : import com.google.gerrit.server.permissions.PermissionBackendException;
      51             : import com.google.gerrit.server.project.ProjectCache;
      52             : import com.google.gerrit.server.ssh.NoSshInfo;
      53             : import com.google.gerrit.server.update.BatchUpdateOp;
      54             : import com.google.gerrit.server.update.ChangeContext;
      55             : import com.google.gerrit.server.update.PostUpdateContext;
      56             : import com.google.gerrit.server.update.RepoContext;
      57             : import com.google.gerrit.server.validators.ValidationException;
      58             : import com.google.inject.Inject;
      59             : import com.google.inject.assistedinject.Assisted;
      60             : import java.io.IOException;
      61             : import java.util.Collections;
      62             : import java.util.List;
      63             : import java.util.Optional;
      64             : import org.eclipse.jgit.lib.ObjectId;
      65             : import org.eclipse.jgit.transport.ReceiveCommand;
      66             : 
      67             : public class PatchSetInserter implements BatchUpdateOp {
      68             :   public interface Factory {
      69             :     PatchSetInserter create(ChangeNotes notes, PatchSet.Id psId, ObjectId commitId);
      70             :   }
      71             : 
      72             :   // Injected fields.
      73             :   private final PermissionBackend permissionBackend;
      74             :   private final PatchSetInfoFactory patchSetInfoFactory;
      75             :   private final ChangeKindCache changeKindCache;
      76             :   private final CommitValidators.Factory commitValidatorsFactory;
      77             :   private final EmailNewPatchSet.Factory emailNewPatchSetFactory;
      78             :   private final ProjectCache projectCache;
      79             :   private final RevisionCreated revisionCreated;
      80             :   private final ApprovalsUtil approvalsUtil;
      81             :   private final ChangeMessagesUtil cmUtil;
      82             :   private final PatchSetUtil psUtil;
      83             :   private final WorkInProgressStateChanged wipStateChanged;
      84             :   private final AutoMerger autoMerger;
      85             : 
      86             :   // Assisted-injected fields.
      87             :   private final PatchSet.Id psId;
      88             :   private final ObjectId commitId;
      89             :   // Read prior to running the batch update, so must only be used during
      90             :   // updateRepo; updateChange and later must use the notes from the
      91             :   // ChangeContext.
      92             :   private final ChangeNotes origNotes;
      93             : 
      94             :   // Fields exposed as setters.
      95             :   private String message;
      96             :   private String description;
      97             :   private Boolean workInProgress;
      98          33 :   private boolean validate = true;
      99          33 :   private boolean checkAddPatchSetPermission = true;
     100          33 :   private List<String> groups = Collections.emptyList();
     101          33 :   private ImmutableListMultimap<String, String> validationOptions = ImmutableListMultimap.of();
     102          33 :   private boolean fireRevisionCreated = true;
     103             :   private boolean allowClosed;
     104          33 :   private boolean sendEmail = true;
     105             :   private String topic;
     106          33 :   private boolean storeCopiedVotes = true;
     107             : 
     108             :   // Fields set during some phase of BatchUpdate.Op.
     109             :   private Change change;
     110             :   private PatchSet patchSet;
     111             :   private PatchSetInfo patchSetInfo;
     112             :   private ChangeKind changeKind;
     113             :   private String mailMessage;
     114             :   private ReviewerSet oldReviewers;
     115             :   private boolean oldWorkInProgressState;
     116             :   private ApprovalCopier.Result approvalCopierResult;
     117             :   private ObjectId preUpdateMetaId;
     118             : 
     119             :   @Inject
     120             :   public PatchSetInserter(
     121             :       PermissionBackend permissionBackend,
     122             :       ApprovalsUtil approvalsUtil,
     123             :       ChangeMessagesUtil cmUtil,
     124             :       PatchSetInfoFactory patchSetInfoFactory,
     125             :       ChangeKindCache changeKindCache,
     126             :       CommitValidators.Factory commitValidatorsFactory,
     127             :       EmailNewPatchSet.Factory emailNewPatchSetFactory,
     128             :       PatchSetUtil psUtil,
     129             :       RevisionCreated revisionCreated,
     130             :       ProjectCache projectCache,
     131             :       WorkInProgressStateChanged wipStateChanged,
     132             :       AutoMerger autoMerger,
     133             :       @Assisted ChangeNotes notes,
     134             :       @Assisted PatchSet.Id psId,
     135          33 :       @Assisted ObjectId commitId) {
     136          33 :     this.permissionBackend = permissionBackend;
     137          33 :     this.approvalsUtil = approvalsUtil;
     138          33 :     this.cmUtil = cmUtil;
     139          33 :     this.patchSetInfoFactory = patchSetInfoFactory;
     140          33 :     this.changeKindCache = changeKindCache;
     141          33 :     this.commitValidatorsFactory = commitValidatorsFactory;
     142          33 :     this.emailNewPatchSetFactory = emailNewPatchSetFactory;
     143          33 :     this.psUtil = psUtil;
     144          33 :     this.revisionCreated = revisionCreated;
     145          33 :     this.projectCache = projectCache;
     146          33 :     this.wipStateChanged = wipStateChanged;
     147          33 :     this.autoMerger = autoMerger;
     148             : 
     149          33 :     this.origNotes = notes;
     150          33 :     this.psId = psId;
     151          33 :     this.commitId = commitId.copy();
     152          33 :   }
     153             : 
     154             :   public PatchSet.Id getPatchSetId() {
     155          33 :     return psId;
     156             :   }
     157             : 
     158             :   public PatchSetInserter setMessage(String message) {
     159          32 :     this.message = message;
     160          32 :     return this;
     161             :   }
     162             : 
     163             :   public PatchSetInserter setDescription(String description) {
     164          18 :     this.description = description;
     165          18 :     return this;
     166             :   }
     167             : 
     168             :   public PatchSetInserter setWorkInProgress(boolean workInProgress) {
     169           3 :     this.workInProgress = workInProgress;
     170           3 :     return this;
     171             :   }
     172             : 
     173             :   public PatchSetInserter setValidate(boolean validate) {
     174          19 :     this.validate = validate;
     175          19 :     return this;
     176             :   }
     177             : 
     178             :   public PatchSetInserter setCheckAddPatchSetPermission(boolean checkAddPatchSetPermission) {
     179          15 :     this.checkAddPatchSetPermission = checkAddPatchSetPermission;
     180          15 :     return this;
     181             :   }
     182             : 
     183             :   public PatchSetInserter setGroups(List<String> groups) {
     184           6 :     requireNonNull(groups, "groups may not be null");
     185           6 :     this.groups = groups;
     186           6 :     return this;
     187             :   }
     188             : 
     189             :   public PatchSetInserter setValidationOptions(
     190             :       ImmutableListMultimap<String, String> validationOptions) {
     191          15 :     requireNonNull(validationOptions, "validationOptions may not be null");
     192          15 :     this.validationOptions = validationOptions;
     193          15 :     return this;
     194             :   }
     195             : 
     196             :   public PatchSetInserter setFireRevisionCreated(boolean fireRevisionCreated) {
     197          19 :     this.fireRevisionCreated = fireRevisionCreated;
     198          19 :     return this;
     199             :   }
     200             : 
     201             :   public PatchSetInserter setAllowClosed(boolean allowClosed) {
     202           2 :     this.allowClosed = allowClosed;
     203           2 :     return this;
     204             :   }
     205             : 
     206             :   public PatchSetInserter setSendEmail(boolean sendEmail) {
     207          28 :     this.sendEmail = sendEmail;
     208          28 :     return this;
     209             :   }
     210             : 
     211             :   public PatchSetInserter setTopic(String topic) {
     212           3 :     this.topic = topic;
     213           3 :     return this;
     214             :   }
     215             : 
     216             :   /**
     217             :    * We always want to store copied votes except when the change is getting submitted and a new
     218             :    * patch-set is created on submit (using submit strategies such as "REBASE_ALWAYS"). In such
     219             :    * cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
     220             :    * should not also store the copied votes.
     221             :    */
     222             :   public PatchSetInserter setStoreCopiedVotes(boolean storeCopiedVotes) {
     223          13 :     this.storeCopiedVotes = storeCopiedVotes;
     224          13 :     return this;
     225             :   }
     226             : 
     227             :   public Change getChange() {
     228           8 :     checkState(change != null, "getChange() only valid after executing update");
     229           8 :     return change;
     230             :   }
     231             : 
     232             :   public PatchSet getPatchSet() {
     233          13 :     checkState(patchSet != null, "getPatchSet() only valid after executing update");
     234          13 :     return patchSet;
     235             :   }
     236             : 
     237             :   @Override
     238             :   public void updateRepo(RepoContext ctx)
     239             :       throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     240          33 :     validate(ctx);
     241          33 :     ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName());
     242             : 
     243          33 :     changeKind =
     244          33 :         changeKindCache.getChangeKind(
     245          33 :             ctx.getProject(),
     246          33 :             ctx.getRevWalk(),
     247          33 :             ctx.getRepoView().getConfig(),
     248          33 :             psUtil.current(origNotes).commitId(),
     249             :             commitId);
     250             : 
     251          33 :     Optional<ReceiveCommand> autoMerge =
     252          33 :         autoMerger.createAutoMergeCommitIfNecessary(
     253          33 :             ctx.getRepoView(),
     254          33 :             ctx.getRevWalk(),
     255          33 :             ctx.getInserter(),
     256          33 :             ctx.getRevWalk().parseCommit(commitId));
     257          33 :     if (autoMerge.isPresent()) {
     258           7 :       ctx.addRefUpdate(autoMerge.get());
     259             :     }
     260          33 :   }
     261             : 
     262             :   @Override
     263             :   public boolean updateChange(ChangeContext ctx)
     264             :       throws ResourceConflictException, IOException, BadRequestException {
     265          33 :     preUpdateMetaId = ctx.getNotes().getMetaId();
     266          33 :     change = ctx.getChange();
     267          33 :     ChangeUpdate update = ctx.getUpdate(psId);
     268          33 :     update.setSubjectForCommit("Create patch set " + psId.get());
     269             : 
     270          33 :     if (!change.isNew() && !allowClosed) {
     271           0 :       throw new ResourceConflictException(
     272           0 :           String.format(
     273             :               "Cannot create new patch set of change %s because it is %s",
     274           0 :               change.getId(), ChangeUtil.status(change)));
     275             :     }
     276             : 
     277          33 :     List<String> newGroups = groups;
     278          33 :     if (newGroups.isEmpty()) {
     279          33 :       PatchSet prevPs = psUtil.current(ctx.getNotes());
     280          33 :       if (prevPs != null) {
     281          33 :         newGroups = prevPs.groups();
     282             :       }
     283             :     }
     284          33 :     patchSet =
     285          33 :         psUtil.insert(
     286          33 :             ctx.getRevWalk(), ctx.getUpdate(psId), psId, commitId, newGroups, null, description);
     287             : 
     288          33 :     if (ctx.getNotify(change.getId()).handling() != NotifyHandling.NONE) {
     289          31 :       oldReviewers = approvalsUtil.getReviewers(ctx.getNotes());
     290             :     }
     291             : 
     292          33 :     oldWorkInProgressState = change.isWorkInProgress();
     293          33 :     if (workInProgress != null) {
     294           3 :       change.setWorkInProgress(workInProgress);
     295           3 :       change.setReviewStarted(!workInProgress);
     296           3 :       update.setWorkInProgress(workInProgress);
     297             :     }
     298             : 
     299          33 :     patchSetInfo =
     300          33 :         patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
     301          33 :     if (!allowClosed) {
     302          33 :       change.setStatus(Change.Status.NEW);
     303             :     }
     304          33 :     change.setCurrentPatchSet(patchSetInfo);
     305          33 :     if (topic != null) {
     306           1 :       change.setTopic(topic);
     307             :       try {
     308           1 :         update.setTopic(topic);
     309           0 :       } catch (ValidationException ex) {
     310           0 :         throw new BadRequestException(ex.getMessage());
     311           1 :       }
     312             :     }
     313             : 
     314          33 :     if (storeCopiedVotes) {
     315          32 :       approvalCopierResult =
     316          32 :           approvalsUtil.copyApprovalsToNewPatchSet(
     317          32 :               ctx.getNotes(), patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
     318             :     }
     319             : 
     320          33 :     mailMessage = insertChangeMessage(update, ctx);
     321             : 
     322          33 :     return true;
     323             :   }
     324             : 
     325             :   @Nullable
     326             :   private String insertChangeMessage(ChangeUpdate update, ChangeContext ctx) {
     327          33 :     StringBuilder messageBuilder = new StringBuilder();
     328          33 :     if (message != null) {
     329          32 :       messageBuilder.append(message);
     330             :     }
     331             : 
     332          33 :     if (approvalCopierResult != null) {
     333          32 :       approvalsUtil
     334          32 :           .formatApprovalCopierResult(
     335             :               approvalCopierResult,
     336             :               projectCache
     337          32 :                   .get(ctx.getProject())
     338          32 :                   .orElseThrow(illegalState(ctx.getProject()))
     339          32 :                   .getLabelTypes())
     340          32 :           .ifPresent(
     341             :               msg -> {
     342           5 :                 if (message != null && !message.endsWith("\n")) {
     343           5 :                   messageBuilder.append("\n");
     344             :                 }
     345           5 :                 messageBuilder.append("\n").append(msg);
     346           5 :               });
     347             :     }
     348             : 
     349          33 :     String changeMessage = messageBuilder.toString();
     350          33 :     if (changeMessage.isEmpty()) {
     351           9 :       return null;
     352             :     }
     353             : 
     354          32 :     return cmUtil.setChangeMessage(
     355             :         update,
     356          32 :         messageBuilder.toString(),
     357          32 :         ChangeMessagesUtil.uploadedPatchSetTag(change.isWorkInProgress()));
     358             :   }
     359             : 
     360             :   @Override
     361             :   public void postUpdate(PostUpdateContext ctx) {
     362          33 :     NotifyResolver.Result notify = ctx.getNotify(change.getId());
     363          33 :     if (notify.shouldNotify() && sendEmail) {
     364          28 :       requireNonNull(mailMessage);
     365             : 
     366          28 :       emailNewPatchSetFactory
     367          28 :           .create(
     368             :               ctx,
     369             :               patchSet,
     370             :               mailMessage,
     371          28 :               approvalCopierResult.outdatedApprovals(),
     372          28 :               oldReviewers.byState(REVIEWER),
     373          28 :               oldReviewers.byState(CC),
     374             :               changeKind,
     375             :               preUpdateMetaId)
     376          28 :           .sendAsync();
     377             :     }
     378             : 
     379          33 :     if (fireRevisionCreated) {
     380          32 :       revisionCreated.fire(
     381          32 :           ctx.getChangeData(change), patchSet, ctx.getAccount(), ctx.getWhen(), notify);
     382             :     }
     383             : 
     384          33 :     if (workInProgress != null && oldWorkInProgressState != workInProgress) {
     385           3 :       wipStateChanged.fire(ctx.getChangeData(change), patchSet, ctx.getAccount(), ctx.getWhen());
     386             :     }
     387          33 :   }
     388             : 
     389             :   private void validate(RepoContext ctx)
     390             :       throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     391             :     // Not allowed to create a new patch set if the current patch set is locked.
     392          33 :     psUtil.checkPatchSetNotLocked(origNotes);
     393             : 
     394          33 :     if (checkAddPatchSetPermission) {
     395          30 :       permissionBackend.user(ctx.getUser()).change(origNotes).check(ChangePermission.ADD_PATCH_SET);
     396             :     }
     397          33 :     projectCache
     398          33 :         .get(ctx.getProject())
     399          33 :         .orElseThrow(illegalState(ctx.getProject()))
     400          33 :         .checkStatePermitsWrite();
     401          33 :     if (!validate) {
     402          10 :       return;
     403             :     }
     404             : 
     405          32 :     String refName = getPatchSetId().toRefName();
     406          32 :     try (CommitReceivedEvent event =
     407             :         new CommitReceivedEvent(
     408             :             new ReceiveCommand(
     409          32 :                 ObjectId.zeroId(),
     410             :                 commitId,
     411          32 :                 refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
     412             :             projectCache
     413          32 :                 .get(origNotes.getProjectName())
     414          32 :                 .orElseThrow(illegalState(origNotes.getProjectName()))
     415          32 :                 .getProject(),
     416          32 :             origNotes.getChange().getDest().branch(),
     417             :             validationOptions,
     418          32 :             ctx.getRepoView().getConfig(),
     419          32 :             ctx.getRevWalk().getObjectReader(),
     420             :             commitId,
     421          32 :             ctx.getIdentifiedUser())) {
     422          32 :       commitValidatorsFactory
     423          32 :           .forGerritCommits(
     424          32 :               permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
     425          32 :               origNotes.getChange().getDest(),
     426          32 :               ctx.getIdentifiedUser(),
     427             :               new NoSshInfo(),
     428          32 :               ctx.getRevWalk(),
     429          32 :               origNotes.getChange())
     430          32 :           .validate(event);
     431           2 :     } catch (CommitValidationException e) {
     432           2 :       throw new ResourceConflictException(e.getFullMessage());
     433          32 :     }
     434          32 :   }
     435             : }

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