LCOV - code coverage report
Current view: top level - server/restapi/change - CreateMergePatchSet.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 112 115 97.4 %
Date: 2022-11-19 15:00:39 Functions: 6 6 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2016 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.gerrit.server.project.ProjectCache.illegalState;
      18             : 
      19             : import com.google.common.base.MoreObjects;
      20             : import com.google.common.base.Strings;
      21             : import com.google.common.collect.Iterables;
      22             : import com.google.gerrit.entities.BranchNameKey;
      23             : import com.google.gerrit.entities.Change;
      24             : import com.google.gerrit.entities.PatchSet;
      25             : import com.google.gerrit.entities.Project;
      26             : import com.google.gerrit.exceptions.InvalidMergeStrategyException;
      27             : import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
      28             : import com.google.gerrit.extensions.client.ListChangesOption;
      29             : import com.google.gerrit.extensions.common.ChangeInfo;
      30             : import com.google.gerrit.extensions.common.MergeInput;
      31             : import com.google.gerrit.extensions.common.MergePatchSetInput;
      32             : import com.google.gerrit.extensions.restapi.AuthException;
      33             : import com.google.gerrit.extensions.restapi.BadRequestException;
      34             : import com.google.gerrit.extensions.restapi.MergeConflictException;
      35             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      36             : import com.google.gerrit.extensions.restapi.Response;
      37             : import com.google.gerrit.extensions.restapi.RestApiException;
      38             : import com.google.gerrit.extensions.restapi.RestModifyView;
      39             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      40             : import com.google.gerrit.server.ChangeUtil;
      41             : import com.google.gerrit.server.CurrentUser;
      42             : import com.google.gerrit.server.GerritPersonIdent;
      43             : import com.google.gerrit.server.IdentifiedUser;
      44             : import com.google.gerrit.server.PatchSetUtil;
      45             : import com.google.gerrit.server.change.ChangeFinder;
      46             : import com.google.gerrit.server.change.ChangeJson;
      47             : import com.google.gerrit.server.change.ChangeResource;
      48             : import com.google.gerrit.server.change.NotifyResolver;
      49             : import com.google.gerrit.server.change.PatchSetInserter;
      50             : import com.google.gerrit.server.git.CodeReviewCommit;
      51             : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
      52             : import com.google.gerrit.server.git.GitRepositoryManager;
      53             : import com.google.gerrit.server.git.MergeUtil;
      54             : import com.google.gerrit.server.git.MergeUtilFactory;
      55             : import com.google.gerrit.server.notedb.ChangeNotes;
      56             : import com.google.gerrit.server.permissions.ChangePermission;
      57             : import com.google.gerrit.server.permissions.PermissionBackend;
      58             : import com.google.gerrit.server.permissions.PermissionBackendException;
      59             : import com.google.gerrit.server.permissions.RefPermission;
      60             : import com.google.gerrit.server.project.ProjectCache;
      61             : import com.google.gerrit.server.project.ProjectState;
      62             : import com.google.gerrit.server.restapi.project.CommitsCollection;
      63             : import com.google.gerrit.server.submit.MergeIdenticalTreeException;
      64             : import com.google.gerrit.server.update.BatchUpdate;
      65             : import com.google.gerrit.server.update.UpdateException;
      66             : import com.google.gerrit.server.util.time.TimeUtil;
      67             : import com.google.inject.Inject;
      68             : import com.google.inject.Provider;
      69             : import com.google.inject.Singleton;
      70             : import java.io.IOException;
      71             : import java.time.Instant;
      72             : import java.time.ZoneId;
      73             : import java.util.List;
      74             : import org.eclipse.jgit.lib.ObjectId;
      75             : import org.eclipse.jgit.lib.ObjectInserter;
      76             : import org.eclipse.jgit.lib.ObjectReader;
      77             : import org.eclipse.jgit.lib.PersonIdent;
      78             : import org.eclipse.jgit.lib.Ref;
      79             : import org.eclipse.jgit.lib.Repository;
      80             : import org.eclipse.jgit.revwalk.RevCommit;
      81             : import org.eclipse.jgit.util.ChangeIdUtil;
      82             : 
      83             : @Singleton
      84             : public class CreateMergePatchSet implements RestModifyView<ChangeResource, MergePatchSetInput> {
      85             :   private final BatchUpdate.Factory updateFactory;
      86             :   private final GitRepositoryManager gitManager;
      87             :   private final CommitsCollection commits;
      88             :   private final ZoneId serverZoneId;
      89             :   private final Provider<CurrentUser> user;
      90             :   private final ChangeJson.Factory jsonFactory;
      91             :   private final PatchSetUtil psUtil;
      92             :   private final MergeUtilFactory mergeUtilFactory;
      93             :   private final PatchSetInserter.Factory patchSetInserterFactory;
      94             :   private final ProjectCache projectCache;
      95             :   private final ChangeFinder changeFinder;
      96             :   private final PermissionBackend permissionBackend;
      97             : 
      98             :   @Inject
      99             :   CreateMergePatchSet(
     100             :       BatchUpdate.Factory updateFactory,
     101             :       GitRepositoryManager gitManager,
     102             :       CommitsCollection commits,
     103             :       @GerritPersonIdent PersonIdent myIdent,
     104             :       Provider<CurrentUser> user,
     105             :       ChangeJson.Factory json,
     106             :       PatchSetUtil psUtil,
     107             :       MergeUtilFactory mergeUtilFactory,
     108             :       PatchSetInserter.Factory patchSetInserterFactory,
     109             :       ProjectCache projectCache,
     110             :       ChangeFinder changeFinder,
     111         145 :       PermissionBackend permissionBackend) {
     112         145 :     this.updateFactory = updateFactory;
     113         145 :     this.gitManager = gitManager;
     114         145 :     this.commits = commits;
     115         145 :     this.serverZoneId = myIdent.getZoneId();
     116         145 :     this.user = user;
     117         145 :     this.jsonFactory = json;
     118         145 :     this.psUtil = psUtil;
     119         145 :     this.mergeUtilFactory = mergeUtilFactory;
     120         145 :     this.patchSetInserterFactory = patchSetInserterFactory;
     121         145 :     this.projectCache = projectCache;
     122         145 :     this.changeFinder = changeFinder;
     123         145 :     this.permissionBackend = permissionBackend;
     124         145 :   }
     125             : 
     126             :   @Override
     127             :   public Response<ChangeInfo> apply(ChangeResource rsrc, MergePatchSetInput in)
     128             :       throws IOException, RestApiException, UpdateException, PermissionBackendException {
     129             :     // Not allowed to create a new patch set if the current patch set is locked.
     130           2 :     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
     131             : 
     132           2 :     rsrc.permissions().check(ChangePermission.ADD_PATCH_SET);
     133           2 :     if (in.author != null) {
     134           1 :       permissionBackend
     135           1 :           .currentUser()
     136           1 :           .project(rsrc.getProject())
     137           1 :           .ref(rsrc.getChange().getDest().branch())
     138           1 :           .check(RefPermission.FORGE_AUTHOR);
     139             :     }
     140             : 
     141           2 :     ProjectState projectState =
     142           2 :         projectCache.get(rsrc.getProject()).orElseThrow(illegalState(rsrc.getProject()));
     143           2 :     projectState.checkStatePermitsWrite();
     144             : 
     145           2 :     MergeInput merge = in.merge;
     146           2 :     if (merge == null || Strings.isNullOrEmpty(merge.source)) {
     147           1 :       throw new BadRequestException("merge.source must be non-empty");
     148             :     }
     149           1 :     if (in.author != null
     150           1 :         && (Strings.isNullOrEmpty(in.author.email) || Strings.isNullOrEmpty(in.author.name))) {
     151           1 :       throw new BadRequestException("Author must specify name and email");
     152             :     }
     153           1 :     in.baseChange = Strings.nullToEmpty(in.baseChange).trim();
     154             : 
     155           1 :     PatchSet ps = psUtil.current(rsrc.getNotes());
     156           1 :     Change change = rsrc.getChange();
     157           1 :     Project.NameKey project = change.getProject();
     158           1 :     BranchNameKey dest = change.getDest();
     159           1 :     try (Repository git = gitManager.openRepository(project);
     160           1 :         ObjectInserter oi = git.newObjectInserter();
     161           1 :         ObjectReader reader = oi.newReader();
     162           1 :         CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) {
     163             : 
     164           1 :       RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, merge.source);
     165           1 :       if (!commits.canRead(projectState, git, sourceCommit)) {
     166           0 :         throw new ResourceNotFoundException(
     167             :             "cannot find source commit: " + merge.source + " to merge.");
     168             :       }
     169             : 
     170             :       RevCommit currentPsCommit;
     171           1 :       List<String> groups = null;
     172           1 :       if (!in.inheritParent && !in.baseChange.isEmpty()) {
     173           1 :         PatchSet basePS = findBasePatchSet(in.baseChange);
     174           1 :         currentPsCommit = rw.parseCommit(basePS.commitId());
     175           1 :         groups = basePS.groups();
     176           1 :       } else {
     177           1 :         currentPsCommit = rw.parseCommit(ps.commitId());
     178             :       }
     179             : 
     180           1 :       Instant now = TimeUtil.now();
     181           1 :       IdentifiedUser me = user.get().asIdentifiedUser();
     182             :       PersonIdent author =
     183           1 :           in.author == null
     184           1 :               ? me.newCommitterIdent(now, serverZoneId)
     185           1 :               : new PersonIdent(in.author.name, in.author.email, now, serverZoneId);
     186           1 :       CodeReviewCommit newCommit =
     187           1 :           createMergeCommit(
     188             :               in,
     189             :               projectState,
     190             :               dest,
     191             :               git,
     192             :               oi,
     193             :               rw,
     194             :               currentPsCommit,
     195             :               sourceCommit,
     196             :               author,
     197           1 :               ObjectId.fromString(change.getKey().get().substring(1)));
     198           1 :       oi.flush();
     199             : 
     200           1 :       PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.id());
     201           1 :       PatchSetInserter psInserter =
     202           1 :           patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
     203           1 :       try (BatchUpdate bu = updateFactory.create(project, me, now)) {
     204           1 :         bu.setRepository(git, rw, oi);
     205           1 :         bu.setNotify(NotifyResolver.Result.none());
     206           1 :         psInserter
     207           1 :             .setMessage(messageForChange(nextPsId, newCommit))
     208           1 :             .setWorkInProgress(!newCommit.getFilesWithGitConflicts().isEmpty())
     209           1 :             .setCheckAddPatchSetPermission(false);
     210           1 :         if (groups != null) {
     211           1 :           psInserter.setGroups(groups);
     212             :         }
     213           1 :         bu.addOp(rsrc.getId(), psInserter);
     214           1 :         bu.execute();
     215             :       }
     216             : 
     217           1 :       ChangeJson json = jsonFactory.create(ListChangesOption.CURRENT_REVISION);
     218           1 :       ChangeInfo changeInfo = json.format(psInserter.getChange());
     219           1 :       changeInfo.containsGitConflicts =
     220           1 :           !newCommit.getFilesWithGitConflicts().isEmpty() ? true : null;
     221           1 :       return Response.ok(changeInfo);
     222           1 :     } catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) {
     223           1 :       throw new BadRequestException(e.getMessage());
     224             :     }
     225             :   }
     226             : 
     227             :   private PatchSet findBasePatchSet(String baseChange)
     228             :       throws PermissionBackendException, UnprocessableEntityException {
     229           1 :     List<ChangeNotes> notes = changeFinder.find(baseChange);
     230           1 :     if (notes.size() != 1) {
     231           0 :       throw new UnprocessableEntityException("Base change not found: " + baseChange);
     232             :     }
     233           1 :     ChangeNotes change = Iterables.getOnlyElement(notes);
     234             :     try {
     235           1 :       permissionBackend.currentUser().change(change).check(ChangePermission.READ);
     236           1 :     } catch (AuthException e) {
     237           1 :       throw new UnprocessableEntityException("Read not permitted for " + baseChange, e);
     238           1 :     }
     239           1 :     return psUtil.current(change);
     240             :   }
     241             : 
     242             :   private CodeReviewCommit createMergeCommit(
     243             :       MergePatchSetInput in,
     244             :       ProjectState projectState,
     245             :       BranchNameKey dest,
     246             :       Repository git,
     247             :       ObjectInserter oi,
     248             :       CodeReviewRevWalk rw,
     249             :       RevCommit currentPsCommit,
     250             :       RevCommit sourceCommit,
     251             :       PersonIdent author,
     252             :       ObjectId changeId)
     253             :       throws ResourceNotFoundException, MergeIdenticalTreeException, MergeConflictException,
     254             :           IOException {
     255             : 
     256             :     ObjectId parentCommit;
     257           1 :     if (in.inheritParent) {
     258             :       // inherit first parent from previous patch set
     259           1 :       parentCommit = currentPsCommit.getParent(0);
     260           1 :     } else if (!in.baseChange.isEmpty()) {
     261           1 :       parentCommit = currentPsCommit.getId();
     262             :     } else {
     263             :       // get the current branch tip of destination branch
     264           1 :       Ref destRef = git.getRefDatabase().exactRef(dest.branch());
     265           1 :       if (destRef != null) {
     266           1 :         parentCommit = destRef.getObjectId();
     267             :       } else {
     268           0 :         throw new ResourceNotFoundException("cannot find destination branch");
     269             :       }
     270             :     }
     271           1 :     RevCommit mergeTip = rw.parseCommit(parentCommit);
     272             : 
     273             :     String commitMsg;
     274           1 :     if (Strings.emptyToNull(in.subject) != null) {
     275           1 :       commitMsg = ChangeIdUtil.insertId(in.subject, changeId);
     276             :     } else {
     277             :       // reuse previous patch set commit message
     278           1 :       commitMsg = currentPsCommit.getFullMessage();
     279             :     }
     280             : 
     281           1 :     String mergeStrategy =
     282           1 :         MoreObjects.firstNonNull(
     283           1 :             Strings.emptyToNull(in.merge.strategy),
     284           1 :             mergeUtilFactory.create(projectState).mergeStrategyName());
     285             : 
     286           1 :     return MergeUtil.createMergeCommit(
     287             :         oi,
     288           1 :         git.getConfig(),
     289             :         mergeTip,
     290             :         sourceCommit,
     291             :         mergeStrategy,
     292             :         in.merge.allowConflicts,
     293             :         author,
     294             :         commitMsg,
     295             :         rw);
     296             :   }
     297             : 
     298             :   private static String messageForChange(PatchSet.Id patchSetId, CodeReviewCommit commit) {
     299           1 :     StringBuilder stringBuilder =
     300           1 :         new StringBuilder(String.format("Uploaded patch set %s.", patchSetId.get()));
     301             : 
     302           1 :     if (!commit.getFilesWithGitConflicts().isEmpty()) {
     303           1 :       stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
     304           1 :       commit.getFilesWithGitConflicts().stream()
     305           1 :           .sorted()
     306           1 :           .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
     307             :     }
     308             : 
     309           1 :     return stringBuilder.toString();
     310             :   }
     311             : }

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