LCOV - code coverage report
Current view: top level - server/edit - ChangeEditModifier.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 260 267 97.4 %
Date: 2022-11-19 15:00:39 Functions: 47 47 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2014 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.edit;
      16             : 
      17             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      18             : 
      19             : import com.google.common.base.Charsets;
      20             : import com.google.gerrit.common.Nullable;
      21             : import com.google.gerrit.entities.BooleanProjectConfig;
      22             : import com.google.gerrit.entities.Change;
      23             : import com.google.gerrit.entities.PatchSet;
      24             : import com.google.gerrit.entities.Project;
      25             : import com.google.gerrit.entities.RefNames;
      26             : import com.google.gerrit.extensions.restapi.AuthException;
      27             : import com.google.gerrit.extensions.restapi.BadRequestException;
      28             : import com.google.gerrit.extensions.restapi.MergeConflictException;
      29             : import com.google.gerrit.extensions.restapi.RawInput;
      30             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      31             : import com.google.gerrit.git.LockFailureException;
      32             : import com.google.gerrit.server.ChangeUtil;
      33             : import com.google.gerrit.server.CurrentUser;
      34             : import com.google.gerrit.server.GerritPersonIdent;
      35             : import com.google.gerrit.server.IdentifiedUser;
      36             : import com.google.gerrit.server.PatchSetUtil;
      37             : import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
      38             : import com.google.gerrit.server.edit.tree.DeleteFileModification;
      39             : import com.google.gerrit.server.edit.tree.RenameFileModification;
      40             : import com.google.gerrit.server.edit.tree.RestoreFileModification;
      41             : import com.google.gerrit.server.edit.tree.TreeCreator;
      42             : import com.google.gerrit.server.edit.tree.TreeModification;
      43             : import com.google.gerrit.server.index.change.ChangeIndexer;
      44             : import com.google.gerrit.server.notedb.ChangeNotes;
      45             : import com.google.gerrit.server.permissions.ChangePermission;
      46             : import com.google.gerrit.server.permissions.PermissionBackend;
      47             : import com.google.gerrit.server.permissions.PermissionBackendException;
      48             : import com.google.gerrit.server.project.InvalidChangeOperationException;
      49             : import com.google.gerrit.server.project.ProjectCache;
      50             : import com.google.gerrit.server.util.CommitMessageUtil;
      51             : import com.google.gerrit.server.util.time.TimeUtil;
      52             : import com.google.inject.Inject;
      53             : import com.google.inject.Provider;
      54             : import com.google.inject.Singleton;
      55             : import java.io.IOException;
      56             : import java.time.Instant;
      57             : import java.time.ZoneId;
      58             : import java.util.List;
      59             : import java.util.Objects;
      60             : import java.util.Optional;
      61             : import org.eclipse.jgit.diff.DiffAlgorithm;
      62             : import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
      63             : import org.eclipse.jgit.diff.RawText;
      64             : import org.eclipse.jgit.diff.RawTextComparator;
      65             : import org.eclipse.jgit.dircache.InvalidPathException;
      66             : import org.eclipse.jgit.lib.BatchRefUpdate;
      67             : import org.eclipse.jgit.lib.CommitBuilder;
      68             : import org.eclipse.jgit.lib.NullProgressMonitor;
      69             : import org.eclipse.jgit.lib.ObjectId;
      70             : import org.eclipse.jgit.lib.ObjectInserter;
      71             : import org.eclipse.jgit.lib.PersonIdent;
      72             : import org.eclipse.jgit.lib.RefUpdate;
      73             : import org.eclipse.jgit.lib.Repository;
      74             : import org.eclipse.jgit.merge.MergeAlgorithm;
      75             : import org.eclipse.jgit.merge.MergeChunk;
      76             : import org.eclipse.jgit.merge.MergeResult;
      77             : import org.eclipse.jgit.merge.MergeStrategy;
      78             : import org.eclipse.jgit.merge.ThreeWayMerger;
      79             : import org.eclipse.jgit.revwalk.RevCommit;
      80             : import org.eclipse.jgit.revwalk.RevTree;
      81             : import org.eclipse.jgit.revwalk.RevWalk;
      82             : import org.eclipse.jgit.transport.ReceiveCommand;
      83             : 
      84             : /**
      85             :  * Utility functions to manipulate change edits.
      86             :  *
      87             :  * <p>This class contains methods to modify edit's content. For retrieving, publishing and deleting
      88             :  * edit see {@link ChangeEditUtil}.
      89             :  *
      90             :  * <p>
      91             :  */
      92             : @Singleton
      93             : public class ChangeEditModifier {
      94             : 
      95             :   private final ZoneId zoneId;
      96             :   private final Provider<CurrentUser> currentUser;
      97             :   private final PermissionBackend permissionBackend;
      98             :   private final ChangeEditUtil changeEditUtil;
      99             :   private final PatchSetUtil patchSetUtil;
     100             :   private final ProjectCache projectCache;
     101             :   private final NoteDbEdits noteDbEdits;
     102             : 
     103             :   @Inject
     104             :   ChangeEditModifier(
     105             :       @GerritPersonIdent PersonIdent gerritIdent,
     106             :       ChangeIndexer indexer,
     107             :       Provider<CurrentUser> currentUser,
     108             :       PermissionBackend permissionBackend,
     109             :       ChangeEditUtil changeEditUtil,
     110             :       PatchSetUtil patchSetUtil,
     111         145 :       ProjectCache projectCache) {
     112         145 :     this.currentUser = currentUser;
     113         145 :     this.permissionBackend = permissionBackend;
     114         145 :     this.zoneId = gerritIdent.getZoneId();
     115         145 :     this.changeEditUtil = changeEditUtil;
     116         145 :     this.patchSetUtil = patchSetUtil;
     117         145 :     this.projectCache = projectCache;
     118             : 
     119         145 :     noteDbEdits = new NoteDbEdits(zoneId, indexer, currentUser);
     120         145 :   }
     121             : 
     122             :   /**
     123             :    * Creates a new change edit.
     124             :    *
     125             :    * @param repository the affected Git repository
     126             :    * @param notes the {@link ChangeNotes} of the change for which the change edit should be created
     127             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     128             :    * @throws InvalidChangeOperationException if a change edit already existed for the change
     129             :    */
     130             :   public void createEdit(Repository repository, ChangeNotes notes)
     131             :       throws AuthException, IOException, InvalidChangeOperationException,
     132             :           PermissionBackendException, ResourceConflictException {
     133          18 :     assertCanEdit(notes);
     134             : 
     135          18 :     Optional<ChangeEdit> changeEdit = lookupChangeEdit(notes);
     136          18 :     if (changeEdit.isPresent()) {
     137           1 :       throw new InvalidChangeOperationException(
     138           1 :           String.format("A change edit already exists for change %s", notes.getChangeId()));
     139             :     }
     140             : 
     141          18 :     PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
     142          18 :     ObjectId patchSetCommitId = currentPatchSet.commitId();
     143          18 :     noteDbEdits.createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.now());
     144          18 :   }
     145             : 
     146             :   /**
     147             :    * Rebase change edit on latest patch set
     148             :    *
     149             :    * @param repository the affected Git repository
     150             :    * @param notes the {@link ChangeNotes} of the change whose change edit should be rebased
     151             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     152             :    * @throws InvalidChangeOperationException if a change edit doesn't exist for the specified
     153             :    *     change, the change edit is already based on the latest patch set, or the change represents
     154             :    *     the root commit
     155             :    */
     156             :   public void rebaseEdit(Repository repository, ChangeNotes notes)
     157             :       throws AuthException, InvalidChangeOperationException, IOException,
     158             :           PermissionBackendException, ResourceConflictException {
     159           2 :     assertCanEdit(notes);
     160             : 
     161           2 :     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
     162           2 :     if (!optionalChangeEdit.isPresent()) {
     163           0 :       throw new InvalidChangeOperationException(
     164           0 :           String.format("No change edit exists for change %s", notes.getChangeId()));
     165             :     }
     166           2 :     ChangeEdit changeEdit = optionalChangeEdit.get();
     167             : 
     168           2 :     PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
     169           2 :     if (isBasedOn(changeEdit, currentPatchSet)) {
     170           1 :       throw new InvalidChangeOperationException(
     171           1 :           String.format(
     172             :               "Change edit for change %s is already based on latest patch set %s",
     173           1 :               notes.getChangeId(), currentPatchSet.id()));
     174             :     }
     175             : 
     176           1 :     rebase(repository, changeEdit, currentPatchSet);
     177           1 :   }
     178             : 
     179             :   private void rebase(Repository repository, ChangeEdit changeEdit, PatchSet currentPatchSet)
     180             :       throws IOException, MergeConflictException, InvalidChangeOperationException {
     181           1 :     RevCommit currentEditCommit = changeEdit.getEditCommit();
     182           1 :     if (currentEditCommit.getParentCount() == 0) {
     183           0 :       throw new InvalidChangeOperationException(
     184             :           "Rebase change edit against root commit not supported");
     185             :     }
     186             : 
     187           1 :     RevCommit basePatchSetCommit = NoteDbEdits.lookupCommit(repository, currentPatchSet.commitId());
     188           1 :     RevTree basePatchSetTree = basePatchSetCommit.getTree();
     189             : 
     190           1 :     ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree);
     191           1 :     Instant nowTimestamp = TimeUtil.now();
     192           1 :     String commitMessage = currentEditCommit.getFullMessage();
     193           1 :     ObjectId newEditCommitId =
     194           1 :         createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
     195             : 
     196           1 :     noteDbEdits.baseEditOnDifferentPatchset(
     197             :         repository, changeEdit, currentPatchSet, currentEditCommit, newEditCommitId, nowTimestamp);
     198           1 :   }
     199             : 
     200             :   /**
     201             :    * Modifies the commit message of a change edit. If the change edit doesn't exist, a new one will
     202             :    * be created based on the current patch set.
     203             :    *
     204             :    * @param repository the affected Git repository
     205             :    * @param notes the {@link ChangeNotes} of the change whose change edit's message should be
     206             :    *     modified
     207             :    * @param newCommitMessage the new commit message
     208             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     209             :    * @throws InvalidChangeOperationException if the commit message is the same as before
     210             :    * @throws BadRequestException if the commit message is malformed
     211             :    */
     212             :   public void modifyMessage(Repository repository, ChangeNotes notes, String newCommitMessage)
     213             :       throws AuthException, IOException, InvalidChangeOperationException,
     214             :           PermissionBackendException, BadRequestException, ResourceConflictException {
     215           6 :     modifyCommit(
     216             :         repository,
     217             :         notes,
     218             :         new ModificationIntention.LatestCommit(),
     219           6 :         CommitModification.builder().newCommitMessage(newCommitMessage).build());
     220           6 :   }
     221             : 
     222             :   /**
     223             :    * Modifies the contents of a file of a change edit. If the change edit doesn't exist, a new one
     224             :    * will be created based on the current patch set.
     225             :    *
     226             :    * @param repository the affected Git repository
     227             :    * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
     228             :    * @param filePath the path of the file whose contents should be modified
     229             :    * @param newContent the new file content
     230             :    * @param newGitFileMode the new file mode in octal format. {@code null} indicates no change
     231             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     232             :    * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
     233             :    * @throws InvalidChangeOperationException if the file already had the specified content
     234             :    * @throws ResourceConflictException if the project state does not permit the operation
     235             :    */
     236             :   public void modifyFile(
     237             :       Repository repository,
     238             :       ChangeNotes notes,
     239             :       String filePath,
     240             :       RawInput newContent,
     241             :       @Nullable Integer newGitFileMode)
     242             :       throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
     243             :           PermissionBackendException, ResourceConflictException {
     244          18 :     modifyCommit(
     245             :         repository,
     246             :         notes,
     247             :         new ModificationIntention.LatestCommit(),
     248          18 :         CommitModification.builder()
     249          18 :             .addTreeModification(
     250             :                 new ChangeFileContentModification(filePath, newContent, newGitFileMode))
     251          18 :             .build());
     252          18 :   }
     253             : 
     254             :   /**
     255             :    * Deletes a file from the Git tree of a change edit. If the change edit doesn't exist, a new one
     256             :    * will be created based on the current patch set.
     257             :    *
     258             :    * @param repository the affected Git repository
     259             :    * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
     260             :    * @param file path of the file which should be deleted
     261             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     262             :    * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
     263             :    * @throws InvalidChangeOperationException if the file does not exist
     264             :    * @throws ResourceConflictException if the project state does not permit the operation
     265             :    */
     266             :   public void deleteFile(Repository repository, ChangeNotes notes, String file)
     267             :       throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
     268             :           PermissionBackendException, ResourceConflictException {
     269           7 :     modifyCommit(
     270             :         repository,
     271             :         notes,
     272             :         new ModificationIntention.LatestCommit(),
     273           7 :         CommitModification.builder().addTreeModification(new DeleteFileModification(file)).build());
     274           6 :   }
     275             : 
     276             :   /**
     277             :    * Renames a file of a change edit or moves it to another directory. If the change edit doesn't
     278             :    * exist, a new one will be created based on the current patch set.
     279             :    *
     280             :    * @param repository the affected Git repository
     281             :    * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
     282             :    * @param currentFilePath the current path/name of the file
     283             :    * @param newFilePath the desired path/name of the file
     284             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     285             :    * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
     286             :    * @throws InvalidChangeOperationException if the file was already renamed to the specified new
     287             :    *     name
     288             :    * @throws ResourceConflictException if the project state does not permit the operation
     289             :    */
     290             :   public void renameFile(
     291             :       Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath)
     292             :       throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
     293             :           PermissionBackendException, ResourceConflictException {
     294           3 :     modifyCommit(
     295             :         repository,
     296             :         notes,
     297             :         new ModificationIntention.LatestCommit(),
     298           3 :         CommitModification.builder()
     299           3 :             .addTreeModification(new RenameFileModification(currentFilePath, newFilePath))
     300           3 :             .build());
     301           3 :   }
     302             : 
     303             :   /**
     304             :    * Restores a file of a change edit to the state it was in before the patch set on which the
     305             :    * change edit is based. If the change edit doesn't exist, a new one will be created based on the
     306             :    * current patch set.
     307             :    *
     308             :    * @param repository the affected Git repository
     309             :    * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
     310             :    * @param file the path of the file which should be restored
     311             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     312             :    * @throws InvalidChangeOperationException if the file was already restored
     313             :    */
     314             :   public void restoreFile(Repository repository, ChangeNotes notes, String file)
     315             :       throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
     316             :           PermissionBackendException, ResourceConflictException {
     317           1 :     modifyCommit(
     318             :         repository,
     319             :         notes,
     320             :         new ModificationIntention.LatestCommit(),
     321           1 :         CommitModification.builder()
     322           1 :             .addTreeModification(new RestoreFileModification(file))
     323           1 :             .build());
     324           1 :   }
     325             : 
     326             :   /**
     327             :    * Applies the indicated modifications to the specified patch set. If a change edit exists and is
     328             :    * based on the same patch set, the modified patch set tree is merged with the change edit. If the
     329             :    * change edit doesn't exist, a new one will be created.
     330             :    *
     331             :    * @param repository the affected Git repository
     332             :    * @param notes the {@link ChangeNotes} of the change to which the patch set belongs
     333             :    * @param patchSet the {@code PatchSet} which should be modified
     334             :    * @param commitModification the modifications which should be applied
     335             :    * @return the resulting {@code ChangeEdit}
     336             :    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
     337             :    * @throws InvalidChangeOperationException if the existing change edit is based on another patch
     338             :    *     set or no change edit exists but the specified patch set isn't the current one
     339             :    * @throws MergeConflictException if the modified patch set tree can't be merged with an existing
     340             :    *     change edit
     341             :    */
     342             :   public ChangeEdit combineWithModifiedPatchSetTree(
     343             :       Repository repository,
     344             :       ChangeNotes notes,
     345             :       PatchSet patchSet,
     346             :       CommitModification commitModification)
     347             :       throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
     348             :           PermissionBackendException, ResourceConflictException {
     349           3 :     return modifyCommit(
     350             :         repository, notes, new ModificationIntention.PatchsetCommit(patchSet), commitModification);
     351             :   }
     352             : 
     353             :   private ChangeEdit modifyCommit(
     354             :       Repository repository,
     355             :       ChangeNotes notes,
     356             :       ModificationIntention modificationIntention,
     357             :       CommitModification commitModification)
     358             :       throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
     359             :           PermissionBackendException, ResourceConflictException {
     360          22 :     assertCanEdit(notes);
     361             : 
     362          22 :     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
     363          22 :     EditBehavior editBehavior =
     364             :         optionalChangeEdit
     365          22 :             .<EditBehavior>map(changeEdit -> new ExistingEditBehavior(changeEdit, noteDbEdits))
     366          22 :             .orElseGet(() -> new NewEditBehavior(noteDbEdits));
     367          22 :     ModificationTarget modificationTarget =
     368          22 :         editBehavior.getModificationTarget(notes, modificationIntention);
     369             : 
     370          22 :     RevCommit commitToModify = modificationTarget.getCommit(repository);
     371          22 :     ObjectId newTreeId =
     372          21 :         createNewTree(repository, commitToModify, commitModification.treeModifications());
     373          21 :     newTreeId = editBehavior.mergeTreesIfNecessary(repository, newTreeId, commitToModify);
     374             : 
     375          21 :     PatchSet basePatchset = modificationTarget.getBasePatchset();
     376          21 :     RevCommit basePatchsetCommit = NoteDbEdits.lookupCommit(repository, basePatchset.commitId());
     377             : 
     378          21 :     boolean changeIdRequired =
     379             :         projectCache
     380          21 :             .get(notes.getChange().getProject())
     381          21 :             .orElseThrow(illegalState(notes.getChange().getProject()))
     382          21 :             .is(BooleanProjectConfig.REQUIRE_CHANGE_ID);
     383          21 :     String currentChangeId = notes.getChange().getKey().get();
     384          21 :     String newCommitMessage =
     385          21 :         createNewCommitMessage(
     386             :             changeIdRequired, currentChangeId, editBehavior, commitModification, commitToModify);
     387          21 :     newCommitMessage = editBehavior.mergeCommitMessageIfNecessary(newCommitMessage, commitToModify);
     388             : 
     389          21 :     Optional<ChangeEdit> unmodifiedEdit =
     390          21 :         editBehavior.getEditIfNoModification(newTreeId, newCommitMessage);
     391          21 :     if (unmodifiedEdit.isPresent()) {
     392           2 :       return unmodifiedEdit.get();
     393             :     }
     394             : 
     395          21 :     Instant nowTimestamp = TimeUtil.now();
     396          21 :     ObjectId newEditCommit =
     397          21 :         createCommit(repository, basePatchsetCommit, newTreeId, newCommitMessage, nowTimestamp);
     398             : 
     399          21 :     return editBehavior.updateEditInStorage(
     400             :         repository, notes, basePatchset, newEditCommit, nowTimestamp);
     401             :   }
     402             : 
     403             :   private void assertCanEdit(ChangeNotes notes)
     404             :       throws AuthException, PermissionBackendException, ResourceConflictException {
     405          24 :     if (!currentUser.get().isIdentifiedUser()) {
     406           0 :       throw new AuthException("Authentication required");
     407             :     }
     408             : 
     409          24 :     Change c = notes.getChange();
     410          24 :     if (!c.isNew()) {
     411           1 :       throw new ResourceConflictException(
     412           1 :           String.format("change %s is %s", c.getChangeId(), ChangeUtil.status(c)));
     413             :     }
     414             : 
     415             :     // Not allowed to edit if the current patch set is locked.
     416          24 :     patchSetUtil.checkPatchSetNotLocked(notes);
     417          24 :     boolean canEdit =
     418          24 :         permissionBackend.currentUser().change(notes).test(ChangePermission.ADD_PATCH_SET);
     419          24 :     canEdit &=
     420             :         projectCache
     421          24 :             .get(notes.getProjectName())
     422          24 :             .orElseThrow(illegalState(notes.getProjectName()))
     423          24 :             .statePermitsWrite();
     424          24 :     if (!canEdit) {
     425           2 :       throw new AuthException("edit not permitted");
     426             :     }
     427          24 :   }
     428             : 
     429             :   private Optional<ChangeEdit> lookupChangeEdit(ChangeNotes notes)
     430             :       throws AuthException, IOException {
     431          24 :     return changeEditUtil.byChange(notes);
     432             :   }
     433             : 
     434             :   private PatchSet lookupCurrentPatchSet(ChangeNotes notes) {
     435          18 :     return patchSetUtil.current(notes);
     436             :   }
     437             : 
     438             :   private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) {
     439           2 :     PatchSet editBasePatchSet = changeEdit.getBasePatchSet();
     440           2 :     return editBasePatchSet.id().equals(patchSet.id());
     441             :   }
     442             : 
     443             :   private static ObjectId createNewTree(
     444             :       Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications)
     445             :       throws BadRequestException, IOException, InvalidChangeOperationException {
     446          22 :     if (treeModifications.isEmpty()) {
     447           6 :       return baseCommit.getTree();
     448             :     }
     449             : 
     450             :     ObjectId newTreeId;
     451             :     try {
     452          21 :       TreeCreator treeCreator = TreeCreator.basedOn(baseCommit);
     453          21 :       treeCreator.addTreeModifications(treeModifications);
     454          21 :       newTreeId = treeCreator.createNewTreeAndGetId(repository);
     455           1 :     } catch (InvalidPathException e) {
     456           1 :       throw new BadRequestException(e.getMessage());
     457          21 :     }
     458             : 
     459          21 :     if (ObjectId.isEqual(newTreeId, baseCommit.getTree())) {
     460           2 :       throw new InvalidChangeOperationException("no changes were made");
     461             :     }
     462          20 :     return newTreeId;
     463             :   }
     464             : 
     465             :   private static ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId)
     466             :       throws IOException, MergeConflictException {
     467           3 :     PatchSet basePatchSet = changeEdit.getBasePatchSet();
     468           3 :     ObjectId basePatchSetCommitId = basePatchSet.commitId();
     469           3 :     ObjectId editCommitId = changeEdit.getEditCommit();
     470             : 
     471           3 :     ThreeWayMerger threeWayMerger = MergeStrategy.RESOLVE.newMerger(repository, true);
     472           3 :     threeWayMerger.setBase(basePatchSetCommitId);
     473           3 :     boolean successful = threeWayMerger.merge(newTreeId, editCommitId);
     474             : 
     475           3 :     if (!successful) {
     476           2 :       throw new MergeConflictException(
     477             :           "The existing change edit could not be merged with another tree.");
     478             :     }
     479           3 :     return threeWayMerger.getResultTreeId();
     480             :   }
     481             : 
     482             :   private String createNewCommitMessage(
     483             :       boolean requireChangeId,
     484             :       String currentChangeId,
     485             :       EditBehavior editBehavior,
     486             :       CommitModification commitModification,
     487             :       RevCommit commitToModify)
     488             :       throws InvalidChangeOperationException, BadRequestException, ResourceConflictException {
     489          21 :     if (!commitModification.newCommitMessage().isPresent()) {
     490          20 :       return editBehavior.getUnmodifiedCommitMessage(commitToModify);
     491             :     }
     492             : 
     493           6 :     String newCommitMessage =
     494           6 :         CommitMessageUtil.checkAndSanitizeCommitMessage(
     495           6 :             commitModification.newCommitMessage().get());
     496             : 
     497           6 :     if (newCommitMessage.equals(commitToModify.getFullMessage())) {
     498           1 :       throw new InvalidChangeOperationException(
     499             :           "New commit message cannot be same as existing commit message");
     500             :     }
     501             : 
     502           6 :     ChangeUtil.ensureChangeIdIsCorrect(requireChangeId, currentChangeId, newCommitMessage);
     503             : 
     504           6 :     return newCommitMessage;
     505             :   }
     506             : 
     507             :   private ObjectId createCommit(
     508             :       Repository repository,
     509             :       RevCommit basePatchsetCommit,
     510             :       ObjectId tree,
     511             :       String commitMessage,
     512             :       Instant timestamp)
     513             :       throws IOException {
     514          21 :     try (ObjectInserter objectInserter = repository.newObjectInserter()) {
     515          21 :       CommitBuilder builder = new CommitBuilder();
     516          21 :       builder.setTreeId(tree);
     517          21 :       builder.setParentIds(basePatchsetCommit.getParents());
     518          21 :       builder.setAuthor(basePatchsetCommit.getAuthorIdent());
     519          21 :       builder.setCommitter(getCommitterIdent(timestamp));
     520          21 :       builder.setMessage(commitMessage);
     521          21 :       ObjectId newCommitId = objectInserter.insert(builder);
     522          21 :       objectInserter.flush();
     523          21 :       return newCommitId;
     524             :     }
     525             :   }
     526             : 
     527             :   private PersonIdent getCommitterIdent(Instant commitTimestamp) {
     528          21 :     IdentifiedUser user = currentUser.get().asIdentifiedUser();
     529          21 :     return user.newCommitterIdent(commitTimestamp, zoneId);
     530             :   }
     531             : 
     532             :   /**
     533             :    * Strategy to apply depending on the current situation regarding change edits (e.g. creating a
     534             :    * new edit requires different storage modifications than updating an existing edit).
     535             :    */
     536             :   private interface EditBehavior {
     537             : 
     538             :     ModificationTarget getModificationTarget(
     539             :         ChangeNotes notes, ModificationIntention targetIntention)
     540             :         throws InvalidChangeOperationException;
     541             : 
     542             :     ObjectId mergeTreesIfNecessary(
     543             :         Repository repository, ObjectId newTreeId, ObjectId commitToModify)
     544             :         throws IOException, MergeConflictException;
     545             : 
     546             :     String getUnmodifiedCommitMessage(RevCommit commitToModify);
     547             : 
     548             :     String mergeCommitMessageIfNecessary(String newCommitMessage, RevCommit commitToModify)
     549             :         throws MergeConflictException;
     550             : 
     551             :     Optional<ChangeEdit> getEditIfNoModification(ObjectId newTreeId, String newCommitMessage);
     552             : 
     553             :     ChangeEdit updateEditInStorage(
     554             :         Repository repository,
     555             :         ChangeNotes notes,
     556             :         PatchSet basePatchSet,
     557             :         ObjectId newEditCommitId,
     558             :         Instant timestamp)
     559             :         throws IOException;
     560             :   }
     561             : 
     562             :   private static class ExistingEditBehavior implements EditBehavior {
     563             : 
     564             :     private final ChangeEdit changeEdit;
     565             :     private final NoteDbEdits noteDbEdits;
     566             : 
     567          13 :     ExistingEditBehavior(ChangeEdit changeEdit, NoteDbEdits noteDbEdits) {
     568          13 :       this.changeEdit = changeEdit;
     569          13 :       this.noteDbEdits = noteDbEdits;
     570          13 :     }
     571             : 
     572             :     @Override
     573             :     public ModificationTarget getModificationTarget(
     574             :         ChangeNotes notes, ModificationIntention targetIntention)
     575             :         throws InvalidChangeOperationException {
     576          13 :       ModificationTarget modificationTarget = targetIntention.getTargetWhenEditExists(changeEdit);
     577             :       // It would be better to do this validation in the implementation of the REST endpoints
     578             :       // before calling any write actions on ChangeEditModifier.
     579          13 :       modificationTarget.ensureTargetMayBeModifiedDespiteExistingEdit(changeEdit);
     580          13 :       return modificationTarget;
     581             :     }
     582             : 
     583             :     @Override
     584             :     public ObjectId mergeTreesIfNecessary(
     585             :         Repository repository, ObjectId newTreeId, ObjectId commitToModify)
     586             :         throws IOException, MergeConflictException {
     587          12 :       if (ObjectId.isEqual(changeEdit.getEditCommit(), commitToModify)) {
     588          12 :         return newTreeId;
     589             :       }
     590           2 :       return merge(repository, changeEdit, newTreeId);
     591             :     }
     592             : 
     593             :     @Override
     594             :     public String getUnmodifiedCommitMessage(RevCommit commitToModify) {
     595          11 :       return changeEdit.getEditCommit().getFullMessage();
     596             :     }
     597             : 
     598             :     @Override
     599             :     public String mergeCommitMessageIfNecessary(String newCommitMessage, RevCommit commitToModify)
     600             :         throws MergeConflictException {
     601          12 :       if (ObjectId.isEqual(changeEdit.getEditCommit(), commitToModify)) {
     602          12 :         return newCommitMessage;
     603             :       }
     604           2 :       String editCommitMessage = changeEdit.getEditCommit().getFullMessage();
     605           2 :       if (editCommitMessage.equals(newCommitMessage)) {
     606           2 :         return editCommitMessage;
     607             :       }
     608           2 :       return mergeCommitMessage(newCommitMessage, commitToModify, editCommitMessage);
     609             :     }
     610             : 
     611             :     private String mergeCommitMessage(
     612             :         String newCommitMessage, RevCommit commitToModify, String editCommitMessage)
     613             :         throws MergeConflictException {
     614           2 :       MergeAlgorithm mergeAlgorithm =
     615           2 :           new MergeAlgorithm(DiffAlgorithm.getAlgorithm(SupportedAlgorithm.MYERS));
     616           2 :       RawText baseMessage = new RawText(commitToModify.getFullMessage().getBytes(Charsets.UTF_8));
     617           2 :       RawText oldMessage = new RawText(editCommitMessage.getBytes(Charsets.UTF_8));
     618           2 :       RawText newMessage = new RawText(newCommitMessage.getBytes(Charsets.UTF_8));
     619           2 :       RawTextComparator textComparator = RawTextComparator.DEFAULT;
     620           2 :       MergeResult<RawText> mergeResult =
     621           2 :           mergeAlgorithm.merge(textComparator, baseMessage, oldMessage, newMessage);
     622           2 :       if (mergeResult.containsConflicts()) {
     623           1 :         throw new MergeConflictException(
     624             :             "The chosen modification adjusted the commit message. However, the new commit message"
     625             :                 + " could not be merged with the commit message of the existing change edit."
     626             :                 + " Please manually apply the desired changes to the commit message of the change"
     627             :                 + " edit.");
     628             :       }
     629             : 
     630           2 :       StringBuilder resultingCommitMessage = new StringBuilder();
     631           2 :       for (MergeChunk mergeChunk : mergeResult) {
     632           2 :         RawText mergedMessagePart = mergeResult.getSequences().get(mergeChunk.getSequenceIndex());
     633           2 :         resultingCommitMessage.append(
     634           2 :             mergedMessagePart.getString(mergeChunk.getBegin(), mergeChunk.getEnd(), false));
     635           2 :       }
     636           2 :       return resultingCommitMessage.toString();
     637             :     }
     638             : 
     639             :     @Override
     640             :     public Optional<ChangeEdit> getEditIfNoModification(
     641             :         ObjectId newTreeId, String newCommitMessage) {
     642          12 :       if (!ObjectId.isEqual(newTreeId, changeEdit.getEditCommit().getTree())) {
     643          11 :         return Optional.empty();
     644             :       }
     645           4 :       if (!Objects.equals(newCommitMessage, changeEdit.getEditCommit().getFullMessage())) {
     646           4 :         return Optional.empty();
     647             :       }
     648             :       // Modifications are already contained in the change edit.
     649           2 :       return Optional.of(changeEdit);
     650             :     }
     651             : 
     652             :     @Override
     653             :     public ChangeEdit updateEditInStorage(
     654             :         Repository repository,
     655             :         ChangeNotes notes,
     656             :         PatchSet basePatchSet,
     657             :         ObjectId newEditCommitId,
     658             :         Instant timestamp)
     659             :         throws IOException {
     660          12 :       return noteDbEdits.updateEdit(
     661          12 :           notes.getProjectName(), repository, changeEdit, newEditCommitId, timestamp);
     662             :     }
     663             :   }
     664             : 
     665             :   private static class NewEditBehavior implements EditBehavior {
     666             : 
     667             :     private final NoteDbEdits noteDbEdits;
     668             : 
     669          17 :     NewEditBehavior(NoteDbEdits noteDbEdits) {
     670          17 :       this.noteDbEdits = noteDbEdits;
     671          17 :     }
     672             : 
     673             :     @Override
     674             :     public ModificationTarget getModificationTarget(
     675             :         ChangeNotes notes, ModificationIntention targetIntention)
     676             :         throws InvalidChangeOperationException {
     677          17 :       ModificationTarget modificationTarget = targetIntention.getTargetWhenNoEdit(notes);
     678             :       // It would be better to do this validation in the implementation of the REST endpoints
     679             :       // before calling any write actions on ChangeEditModifier.
     680          17 :       modificationTarget.ensureNewEditMayBeBasedOnTarget(notes.getChange());
     681          17 :       return modificationTarget;
     682             :     }
     683             : 
     684             :     @Override
     685             :     public ObjectId mergeTreesIfNecessary(
     686             :         Repository repository, ObjectId newTreeId, ObjectId commitToModify) {
     687          17 :       return newTreeId;
     688             :     }
     689             : 
     690             :     @Override
     691             :     public String getUnmodifiedCommitMessage(RevCommit commitToModify) {
     692          17 :       return commitToModify.getFullMessage();
     693             :     }
     694             : 
     695             :     @Override
     696             :     public String mergeCommitMessageIfNecessary(String newCommitMessage, RevCommit commitToModify) {
     697          17 :       return newCommitMessage;
     698             :     }
     699             : 
     700             :     @Override
     701             :     public Optional<ChangeEdit> getEditIfNoModification(
     702             :         ObjectId newTreeId, String newCommitMessage) {
     703          17 :       return Optional.empty();
     704             :     }
     705             : 
     706             :     @Override
     707             :     public ChangeEdit updateEditInStorage(
     708             :         Repository repository,
     709             :         ChangeNotes notes,
     710             :         PatchSet basePatchSet,
     711             :         ObjectId newEditCommitId,
     712             :         Instant timestamp)
     713             :         throws IOException {
     714          17 :       return noteDbEdits.createEdit(repository, notes, basePatchSet, newEditCommitId, timestamp);
     715             :     }
     716             :   }
     717             : 
     718             :   private static class NoteDbEdits {
     719             :     private final ZoneId zoneId;
     720             :     private final ChangeIndexer indexer;
     721             :     private final Provider<CurrentUser> currentUser;
     722             : 
     723         145 :     NoteDbEdits(ZoneId zoneId, ChangeIndexer indexer, Provider<CurrentUser> currentUser) {
     724         145 :       this.zoneId = zoneId;
     725         145 :       this.indexer = indexer;
     726         145 :       this.currentUser = currentUser;
     727         145 :     }
     728             : 
     729             :     ChangeEdit createEdit(
     730             :         Repository repository,
     731             :         ChangeNotes notes,
     732             :         PatchSet basePatchset,
     733             :         ObjectId newEditCommitId,
     734             :         Instant timestamp)
     735             :         throws IOException {
     736          24 :       Change change = notes.getChange();
     737          24 :       String editRefName = getEditRefName(change, basePatchset);
     738          24 :       updateReference(
     739          24 :           notes.getProjectName(),
     740             :           repository,
     741             :           editRefName,
     742          24 :           ObjectId.zeroId(),
     743             :           newEditCommitId,
     744             :           timestamp);
     745          24 :       reindex(change);
     746             : 
     747          24 :       RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
     748          24 :       return new ChangeEdit(change, editRefName, newEditCommit, basePatchset);
     749             :     }
     750             : 
     751             :     private String getEditRefName(Change change, PatchSet basePatchset) {
     752          24 :       IdentifiedUser me = currentUser.get().asIdentifiedUser();
     753          24 :       return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchset.id());
     754             :     }
     755             : 
     756             :     ChangeEdit updateEdit(
     757             :         Project.NameKey projectName,
     758             :         Repository repository,
     759             :         ChangeEdit changeEdit,
     760             :         ObjectId newEditCommitId,
     761             :         Instant timestamp)
     762             :         throws IOException {
     763          12 :       String editRefName = changeEdit.getRefName();
     764          12 :       RevCommit currentEditCommit = changeEdit.getEditCommit();
     765          12 :       updateReference(
     766             :           projectName, repository, editRefName, currentEditCommit, newEditCommitId, timestamp);
     767          12 :       reindex(changeEdit.getChange());
     768             : 
     769          12 :       RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
     770          12 :       return new ChangeEdit(
     771          12 :           changeEdit.getChange(), editRefName, newEditCommit, changeEdit.getBasePatchSet());
     772             :     }
     773             : 
     774             :     private void updateReference(
     775             :         Project.NameKey projectName,
     776             :         Repository repository,
     777             :         String refName,
     778             :         ObjectId currentObjectId,
     779             :         ObjectId targetObjectId,
     780             :         Instant timestamp)
     781             :         throws IOException {
     782          24 :       RefUpdate ru = repository.updateRef(refName);
     783          24 :       ru.setExpectedOldObjectId(currentObjectId);
     784          24 :       ru.setNewObjectId(targetObjectId);
     785          24 :       ru.setRefLogIdent(getRefLogIdent(timestamp));
     786          24 :       ru.setRefLogMessage("inline edit (amend)", false);
     787          24 :       ru.setForceUpdate(true);
     788          24 :       try (RevWalk revWalk = new RevWalk(repository)) {
     789          24 :         RefUpdate.Result res = ru.update(revWalk);
     790          24 :         String message = "cannot update " + ru.getName() + " in " + projectName + ": " + res;
     791          24 :         if (res == RefUpdate.Result.LOCK_FAILURE) {
     792           0 :           throw new LockFailureException(message, ru);
     793             :         }
     794          24 :         if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
     795           0 :           throw new IOException(message);
     796             :         }
     797             :       }
     798          24 :     }
     799             : 
     800             :     void baseEditOnDifferentPatchset(
     801             :         Repository repository,
     802             :         ChangeEdit changeEdit,
     803             :         PatchSet currentPatchSet,
     804             :         ObjectId currentEditCommit,
     805             :         ObjectId newEditCommitId,
     806             :         Instant nowTimestamp)
     807             :         throws IOException {
     808           1 :       String newEditRefName = getEditRefName(changeEdit.getChange(), currentPatchSet);
     809           1 :       updateReferenceWithNameChange(
     810             :           repository,
     811           1 :           changeEdit.getRefName(),
     812             :           currentEditCommit,
     813             :           newEditRefName,
     814             :           newEditCommitId,
     815             :           nowTimestamp);
     816           1 :       reindex(changeEdit.getChange());
     817           1 :     }
     818             : 
     819             :     private void updateReferenceWithNameChange(
     820             :         Repository repository,
     821             :         String currentRefName,
     822             :         ObjectId currentObjectId,
     823             :         String newRefName,
     824             :         ObjectId targetObjectId,
     825             :         Instant timestamp)
     826             :         throws IOException {
     827           1 :       BatchRefUpdate batchRefUpdate = repository.getRefDatabase().newBatchUpdate();
     828           1 :       batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), targetObjectId, newRefName));
     829           1 :       batchRefUpdate.addCommand(
     830           1 :           new ReceiveCommand(currentObjectId, ObjectId.zeroId(), currentRefName));
     831           1 :       batchRefUpdate.setRefLogMessage("rebase edit", false);
     832           1 :       batchRefUpdate.setRefLogIdent(getRefLogIdent(timestamp));
     833           1 :       try (RevWalk revWalk = new RevWalk(repository)) {
     834           1 :         batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
     835             :       }
     836           1 :       for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
     837           1 :         if (cmd.getResult() != ReceiveCommand.Result.OK) {
     838           0 :           throw new IOException("failed: " + cmd);
     839             :         }
     840           1 :       }
     841           1 :     }
     842             : 
     843             :     static RevCommit lookupCommit(Repository repository, ObjectId commitId) throws IOException {
     844          24 :       try (RevWalk revWalk = new RevWalk(repository)) {
     845          24 :         return revWalk.parseCommit(commitId);
     846             :       }
     847             :     }
     848             : 
     849             :     private PersonIdent getRefLogIdent(Instant timestamp) {
     850          24 :       IdentifiedUser user = currentUser.get().asIdentifiedUser();
     851          24 :       return user.newRefLogIdent(timestamp, zoneId);
     852             :     }
     853             : 
     854             :     private void reindex(Change change) {
     855          24 :       indexer.index(change);
     856          24 :     }
     857             :   }
     858             : }

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