| // Copyright (C) 2014 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.edit; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.MergeConflictException; |
| import com.google.gerrit.extensions.restapi.RawInput; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.RefNames; |
| import com.google.gerrit.server.ChangeUtil; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.PatchSetUtil; |
| import com.google.gerrit.server.edit.tree.ChangeFileContentModification; |
| import com.google.gerrit.server.edit.tree.DeleteFileModification; |
| import com.google.gerrit.server.edit.tree.RenameFileModification; |
| import com.google.gerrit.server.edit.tree.RestoreFileModification; |
| import com.google.gerrit.server.edit.tree.TreeCreator; |
| import com.google.gerrit.server.edit.tree.TreeModification; |
| import com.google.gerrit.server.index.change.ChangeIndexer; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.permissions.ChangePermission; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.project.InvalidChangeOperationException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.util.CommitMessageUtil; |
| import com.google.gerrit.server.util.time.TimeUtil; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.TimeZone; |
| import org.eclipse.jgit.dircache.InvalidPathException; |
| import org.eclipse.jgit.lib.BatchRefUpdate; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.NullProgressMonitor; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.merge.MergeStrategy; |
| import org.eclipse.jgit.merge.ThreeWayMerger; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevTree; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.transport.ReceiveCommand; |
| |
| /** |
| * Utility functions to manipulate change edits. |
| * |
| * <p>This class contains methods to modify edit's content. For retrieving, publishing and deleting |
| * edit see {@link ChangeEditUtil}. |
| * |
| * <p> |
| */ |
| @Singleton |
| public class ChangeEditModifier { |
| |
| private final TimeZone tz; |
| private final ChangeIndexer indexer; |
| private final Provider<CurrentUser> currentUser; |
| private final PermissionBackend permissionBackend; |
| private final ChangeEditUtil changeEditUtil; |
| private final PatchSetUtil patchSetUtil; |
| private final ProjectCache projectCache; |
| |
| @Inject |
| ChangeEditModifier( |
| @GerritPersonIdent PersonIdent gerritIdent, |
| ChangeIndexer indexer, |
| Provider<CurrentUser> currentUser, |
| PermissionBackend permissionBackend, |
| ChangeEditUtil changeEditUtil, |
| PatchSetUtil patchSetUtil, |
| ProjectCache projectCache) { |
| this.indexer = indexer; |
| this.currentUser = currentUser; |
| this.permissionBackend = permissionBackend; |
| this.tz = gerritIdent.getTimeZone(); |
| this.changeEditUtil = changeEditUtil; |
| this.patchSetUtil = patchSetUtil; |
| this.projectCache = projectCache; |
| } |
| |
| /** |
| * Creates a new change edit. |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change for which the change edit should be created |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws InvalidChangeOperationException if a change edit already existed for the change |
| * @throws PermissionBackendException |
| */ |
| public void createEdit(Repository repository, ChangeNotes notes) |
| throws AuthException, IOException, InvalidChangeOperationException, |
| PermissionBackendException, ResourceConflictException { |
| assertCanEdit(notes); |
| |
| Optional<ChangeEdit> changeEdit = lookupChangeEdit(notes); |
| if (changeEdit.isPresent()) { |
| throw new InvalidChangeOperationException( |
| String.format("A change edit already exists for change %s", notes.getChangeId())); |
| } |
| |
| PatchSet currentPatchSet = lookupCurrentPatchSet(notes); |
| ObjectId patchSetCommitId = getPatchSetCommitId(currentPatchSet); |
| createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.nowTs()); |
| } |
| |
| /** |
| * Rebase change edit on latest patch set |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change whose change edit should be rebased |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws InvalidChangeOperationException if a change edit doesn't exist for the specified |
| * change, the change edit is already based on the latest patch set, or the change represents |
| * the root commit |
| * @throws MergeConflictException if rebase fails due to merge conflicts |
| * @throws PermissionBackendException |
| */ |
| public void rebaseEdit(Repository repository, ChangeNotes notes) |
| throws AuthException, InvalidChangeOperationException, IOException, MergeConflictException, |
| PermissionBackendException, ResourceConflictException { |
| assertCanEdit(notes); |
| |
| Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes); |
| if (!optionalChangeEdit.isPresent()) { |
| throw new InvalidChangeOperationException( |
| String.format("No change edit exists for change %s", notes.getChangeId())); |
| } |
| ChangeEdit changeEdit = optionalChangeEdit.get(); |
| |
| PatchSet currentPatchSet = lookupCurrentPatchSet(notes); |
| if (isBasedOn(changeEdit, currentPatchSet)) { |
| throw new InvalidChangeOperationException( |
| String.format( |
| "Change edit for change %s is already based on latest patch set %s", |
| notes.getChangeId(), currentPatchSet.getId())); |
| } |
| |
| rebase(repository, changeEdit, currentPatchSet); |
| } |
| |
| private void rebase(Repository repository, ChangeEdit changeEdit, PatchSet currentPatchSet) |
| throws IOException, MergeConflictException, InvalidChangeOperationException { |
| RevCommit currentEditCommit = changeEdit.getEditCommit(); |
| if (currentEditCommit.getParentCount() == 0) { |
| throw new InvalidChangeOperationException( |
| "Rebase change edit against root commit not supported"); |
| } |
| |
| Change change = changeEdit.getChange(); |
| RevCommit basePatchSetCommit = lookupCommit(repository, currentPatchSet); |
| RevTree basePatchSetTree = basePatchSetCommit.getTree(); |
| |
| ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree); |
| Timestamp nowTimestamp = TimeUtil.nowTs(); |
| String commitMessage = currentEditCommit.getFullMessage(); |
| ObjectId newEditCommitId = |
| createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp); |
| |
| String newEditRefName = getEditRefName(change, currentPatchSet); |
| updateReferenceWithNameChange( |
| repository, |
| changeEdit.getRefName(), |
| currentEditCommit, |
| newEditRefName, |
| newEditCommitId, |
| nowTimestamp); |
| reindex(change); |
| } |
| |
| /** |
| * Modifies the commit message of a change edit. If the change edit doesn't exist, a new one will |
| * be created based on the current patch set. |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change whose change edit's message should be |
| * modified |
| * @param newCommitMessage the new commit message |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws UnchangedCommitMessageException if the commit message is the same as before |
| * @throws PermissionBackendException |
| * @throws BadRequestException if the commit message is malformed |
| */ |
| public void modifyMessage(Repository repository, ChangeNotes notes, String newCommitMessage) |
| throws AuthException, IOException, UnchangedCommitMessageException, |
| PermissionBackendException, BadRequestException, ResourceConflictException { |
| assertCanEdit(notes); |
| newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage); |
| |
| Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes); |
| PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, notes); |
| RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet); |
| RevCommit baseCommit = |
| optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(basePatchSetCommit); |
| |
| String currentCommitMessage = baseCommit.getFullMessage(); |
| if (newCommitMessage.equals(currentCommitMessage)) { |
| throw new UnchangedCommitMessageException(); |
| } |
| |
| RevTree baseTree = baseCommit.getTree(); |
| Timestamp nowTimestamp = TimeUtil.nowTs(); |
| ObjectId newEditCommit = |
| createCommit(repository, basePatchSetCommit, baseTree, newCommitMessage, nowTimestamp); |
| |
| if (optionalChangeEdit.isPresent()) { |
| updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); |
| } else { |
| createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp); |
| } |
| } |
| |
| /** |
| * Modifies the contents of a file of a change edit. If the change edit doesn't exist, a new one |
| * will be created based on the current patch set. |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change whose change edit should be modified |
| * @param filePath the path of the file whose contents should be modified |
| * @param newContent the new file content |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws BadRequestException if the user provided bad input (e.g. invalid file paths) |
| * @throws InvalidChangeOperationException if the file already had the specified content |
| * @throws PermissionBackendException |
| * @throws ResourceConflictException if the project state does not permit the operation |
| */ |
| public void modifyFile( |
| Repository repository, ChangeNotes notes, String filePath, RawInput newContent) |
| throws AuthException, BadRequestException, InvalidChangeOperationException, IOException, |
| PermissionBackendException, ResourceConflictException { |
| modifyTree(repository, notes, new ChangeFileContentModification(filePath, newContent)); |
| } |
| |
| /** |
| * Deletes a file from the Git tree of a change edit. If the change edit doesn't exist, a new one |
| * will be created based on the current patch set. |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change whose change edit should be modified |
| * @param file path of the file which should be deleted |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws BadRequestException if the user provided bad input (e.g. invalid file paths) |
| * @throws InvalidChangeOperationException if the file does not exist |
| * @throws PermissionBackendException |
| * @throws ResourceConflictException if the project state does not permit the operation |
| */ |
| public void deleteFile(Repository repository, ChangeNotes notes, String file) |
| throws AuthException, BadRequestException, InvalidChangeOperationException, IOException, |
| PermissionBackendException, ResourceConflictException { |
| modifyTree(repository, notes, new DeleteFileModification(file)); |
| } |
| |
| /** |
| * Renames a file of a change edit or moves it to another directory. If the change edit doesn't |
| * exist, a new one will be created based on the current patch set. |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change whose change edit should be modified |
| * @param currentFilePath the current path/name of the file |
| * @param newFilePath the desired path/name of the file |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws BadRequestException if the user provided bad input (e.g. invalid file paths) |
| * @throws InvalidChangeOperationException if the file was already renamed to the specified new |
| * name |
| * @throws PermissionBackendException |
| * @throws ResourceConflictException if the project state does not permit the operation |
| */ |
| public void renameFile( |
| Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath) |
| throws AuthException, BadRequestException, InvalidChangeOperationException, IOException, |
| PermissionBackendException, ResourceConflictException { |
| modifyTree(repository, notes, new RenameFileModification(currentFilePath, newFilePath)); |
| } |
| |
| /** |
| * Restores a file of a change edit to the state it was in before the patch set on which the |
| * change edit is based. If the change edit doesn't exist, a new one will be created based on the |
| * current patch set. |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change whose change edit should be modified |
| * @param file the path of the file which should be restored |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws InvalidChangeOperationException if the file was already restored |
| * @throws PermissionBackendException |
| */ |
| public void restoreFile(Repository repository, ChangeNotes notes, String file) |
| throws AuthException, BadRequestException, InvalidChangeOperationException, IOException, |
| PermissionBackendException, ResourceConflictException { |
| modifyTree(repository, notes, new RestoreFileModification(file)); |
| } |
| |
| private void modifyTree( |
| Repository repository, ChangeNotes notes, TreeModification treeModification) |
| throws AuthException, BadRequestException, IOException, InvalidChangeOperationException, |
| PermissionBackendException, ResourceConflictException { |
| assertCanEdit(notes); |
| |
| Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes); |
| PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, notes); |
| RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet); |
| RevCommit baseCommit = |
| optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(basePatchSetCommit); |
| |
| ObjectId newTreeId = createNewTree(repository, baseCommit, ImmutableList.of(treeModification)); |
| |
| String commitMessage = baseCommit.getFullMessage(); |
| Timestamp nowTimestamp = TimeUtil.nowTs(); |
| ObjectId newEditCommit = |
| createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp); |
| |
| if (optionalChangeEdit.isPresent()) { |
| updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); |
| } else { |
| createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp); |
| } |
| } |
| |
| /** |
| * Applies the indicated modifications to the specified patch set. If a change edit exists and is |
| * based on the same patch set, the modified patch set tree is merged with the change edit. If the |
| * change edit doesn't exist, a new one will be created. |
| * |
| * @param repository the affected Git repository |
| * @param notes the {@link ChangeNotes} of the change to which the patch set belongs |
| * @param patchSet the {@code PatchSet} which should be modified |
| * @param treeModifications the modifications which should be applied |
| * @return the resulting {@code ChangeEdit} |
| * @throws AuthException if the user isn't authenticated or not allowed to use change edits |
| * @throws InvalidChangeOperationException if the existing change edit is based on another patch |
| * set or no change edit exists but the specified patch set isn't the current one |
| * @throws MergeConflictException if the modified patch set tree can't be merged with an existing |
| * change edit |
| */ |
| public ChangeEdit combineWithModifiedPatchSetTree( |
| Repository repository, |
| ChangeNotes notes, |
| PatchSet patchSet, |
| List<TreeModification> treeModifications) |
| throws AuthException, BadRequestException, IOException, InvalidChangeOperationException, |
| MergeConflictException, PermissionBackendException, ResourceConflictException { |
| assertCanEdit(notes); |
| |
| Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes); |
| ensureAllowedPatchSet(notes, optionalChangeEdit, patchSet); |
| |
| RevCommit patchSetCommit = lookupCommit(repository, patchSet); |
| ObjectId newTreeId = createNewTree(repository, patchSetCommit, treeModifications); |
| |
| if (optionalChangeEdit.isPresent()) { |
| ChangeEdit changeEdit = optionalChangeEdit.get(); |
| newTreeId = merge(repository, changeEdit, newTreeId); |
| if (ObjectId.equals(newTreeId, changeEdit.getEditCommit().getTree())) { |
| // Modifications are already contained in the change edit. |
| return changeEdit; |
| } |
| } |
| |
| String commitMessage = |
| optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(patchSetCommit).getFullMessage(); |
| Timestamp nowTimestamp = TimeUtil.nowTs(); |
| ObjectId newEditCommit = |
| createCommit(repository, patchSetCommit, newTreeId, commitMessage, nowTimestamp); |
| |
| if (optionalChangeEdit.isPresent()) { |
| return updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); |
| } |
| return createEdit(repository, notes, patchSet, newEditCommit, nowTimestamp); |
| } |
| |
| private void assertCanEdit(ChangeNotes notes) |
| throws AuthException, PermissionBackendException, IOException, ResourceConflictException { |
| if (!currentUser.get().isIdentifiedUser()) { |
| throw new AuthException("Authentication required"); |
| } |
| |
| Change c = notes.getChange(); |
| if (!c.isNew()) { |
| throw new ResourceConflictException( |
| String.format("change %s is %s", c.getChangeId(), ChangeUtil.status(c))); |
| } |
| |
| // Not allowed to edit if the current patch set is locked. |
| patchSetUtil.checkPatchSetNotLocked(notes); |
| try { |
| permissionBackend.currentUser().change(notes).check(ChangePermission.ADD_PATCH_SET); |
| projectCache.checkedGet(notes.getProjectName()).checkStatePermitsWrite(); |
| } catch (AuthException denied) { |
| throw new AuthException("edit not permitted", denied); |
| } |
| } |
| |
| private static void ensureAllowedPatchSet( |
| ChangeNotes notes, Optional<ChangeEdit> optionalChangeEdit, PatchSet patchSet) |
| throws InvalidChangeOperationException { |
| if (optionalChangeEdit.isPresent()) { |
| ChangeEdit changeEdit = optionalChangeEdit.get(); |
| if (!isBasedOn(changeEdit, patchSet)) { |
| throw new InvalidChangeOperationException( |
| String.format( |
| "Only the patch set %s on which the existing change edit is based may be modified " |
| + "(specified patch set: %s)", |
| changeEdit.getBasePatchSet().getId(), patchSet.getId())); |
| } |
| } else { |
| PatchSet.Id patchSetId = patchSet.getId(); |
| PatchSet.Id currentPatchSetId = notes.getChange().currentPatchSetId(); |
| if (!patchSetId.equals(currentPatchSetId)) { |
| throw new InvalidChangeOperationException( |
| String.format( |
| "A change edit may only be created for the current patch set %s (and not for %s)", |
| currentPatchSetId, patchSetId)); |
| } |
| } |
| } |
| |
| private Optional<ChangeEdit> lookupChangeEdit(ChangeNotes notes) |
| throws AuthException, IOException { |
| return changeEditUtil.byChange(notes); |
| } |
| |
| private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit, ChangeNotes notes) { |
| Optional<PatchSet> editBasePatchSet = optionalChangeEdit.map(ChangeEdit::getBasePatchSet); |
| return editBasePatchSet.isPresent() ? editBasePatchSet.get() : lookupCurrentPatchSet(notes); |
| } |
| |
| private PatchSet lookupCurrentPatchSet(ChangeNotes notes) { |
| return patchSetUtil.current(notes); |
| } |
| |
| private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) { |
| PatchSet editBasePatchSet = changeEdit.getBasePatchSet(); |
| return editBasePatchSet.getId().equals(patchSet.getId()); |
| } |
| |
| private static RevCommit lookupCommit(Repository repository, PatchSet patchSet) |
| throws IOException { |
| ObjectId patchSetCommitId = getPatchSetCommitId(patchSet); |
| return lookupCommit(repository, patchSetCommitId); |
| } |
| |
| private static RevCommit lookupCommit(Repository repository, ObjectId commitId) |
| throws IOException { |
| try (RevWalk revWalk = new RevWalk(repository)) { |
| return revWalk.parseCommit(commitId); |
| } |
| } |
| |
| private static ObjectId createNewTree( |
| Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications) |
| throws BadRequestException, IOException, InvalidChangeOperationException { |
| ObjectId newTreeId; |
| try { |
| TreeCreator treeCreator = new TreeCreator(baseCommit); |
| treeCreator.addTreeModifications(treeModifications); |
| newTreeId = treeCreator.createNewTreeAndGetId(repository); |
| } catch (InvalidPathException e) { |
| throw new BadRequestException(e.getMessage()); |
| } |
| |
| if (ObjectId.equals(newTreeId, baseCommit.getTree())) { |
| throw new InvalidChangeOperationException("no changes were made"); |
| } |
| return newTreeId; |
| } |
| |
| private static ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId) |
| throws IOException, MergeConflictException { |
| PatchSet basePatchSet = changeEdit.getBasePatchSet(); |
| ObjectId basePatchSetCommitId = getPatchSetCommitId(basePatchSet); |
| ObjectId editCommitId = changeEdit.getEditCommit(); |
| |
| ThreeWayMerger threeWayMerger = MergeStrategy.RESOLVE.newMerger(repository, true); |
| threeWayMerger.setBase(basePatchSetCommitId); |
| boolean successful = threeWayMerger.merge(newTreeId, editCommitId); |
| |
| if (!successful) { |
| throw new MergeConflictException( |
| "The existing change edit could not be merged with another tree."); |
| } |
| return threeWayMerger.getResultTreeId(); |
| } |
| |
| private ObjectId createCommit( |
| Repository repository, |
| RevCommit basePatchSetCommit, |
| ObjectId tree, |
| String commitMessage, |
| Timestamp timestamp) |
| throws IOException { |
| try (ObjectInserter objectInserter = repository.newObjectInserter()) { |
| CommitBuilder builder = new CommitBuilder(); |
| builder.setTreeId(tree); |
| builder.setParentIds(basePatchSetCommit.getParents()); |
| builder.setAuthor(basePatchSetCommit.getAuthorIdent()); |
| builder.setCommitter(getCommitterIdent(timestamp)); |
| builder.setMessage(commitMessage); |
| ObjectId newCommitId = objectInserter.insert(builder); |
| objectInserter.flush(); |
| return newCommitId; |
| } |
| } |
| |
| private PersonIdent getCommitterIdent(Timestamp commitTimestamp) { |
| IdentifiedUser user = currentUser.get().asIdentifiedUser(); |
| return user.newCommitterIdent(commitTimestamp, tz); |
| } |
| |
| private static ObjectId getPatchSetCommitId(PatchSet patchSet) { |
| return ObjectId.fromString(patchSet.getRevision().get()); |
| } |
| |
| private ChangeEdit createEdit( |
| Repository repository, |
| ChangeNotes notes, |
| PatchSet basePatchSet, |
| ObjectId newEditCommitId, |
| Timestamp timestamp) |
| throws IOException { |
| Change change = notes.getChange(); |
| String editRefName = getEditRefName(change, basePatchSet); |
| updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommitId, timestamp); |
| reindex(change); |
| |
| RevCommit newEditCommit = lookupCommit(repository, newEditCommitId); |
| return new ChangeEdit(change, editRefName, newEditCommit, basePatchSet); |
| } |
| |
| private String getEditRefName(Change change, PatchSet basePatchSet) { |
| IdentifiedUser me = currentUser.get().asIdentifiedUser(); |
| return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchSet.getId()); |
| } |
| |
| private ChangeEdit updateEdit( |
| Repository repository, ChangeEdit changeEdit, ObjectId newEditCommitId, Timestamp timestamp) |
| throws IOException { |
| String editRefName = changeEdit.getRefName(); |
| RevCommit currentEditCommit = changeEdit.getEditCommit(); |
| updateReference(repository, editRefName, currentEditCommit, newEditCommitId, timestamp); |
| reindex(changeEdit.getChange()); |
| |
| RevCommit newEditCommit = lookupCommit(repository, newEditCommitId); |
| return new ChangeEdit( |
| changeEdit.getChange(), editRefName, newEditCommit, changeEdit.getBasePatchSet()); |
| } |
| |
| private void updateReference( |
| Repository repository, |
| String refName, |
| ObjectId currentObjectId, |
| ObjectId targetObjectId, |
| Timestamp timestamp) |
| throws IOException { |
| RefUpdate ru = repository.updateRef(refName); |
| ru.setExpectedOldObjectId(currentObjectId); |
| ru.setNewObjectId(targetObjectId); |
| ru.setRefLogIdent(getRefLogIdent(timestamp)); |
| ru.setRefLogMessage("inline edit (amend)", false); |
| ru.setForceUpdate(true); |
| try (RevWalk revWalk = new RevWalk(repository)) { |
| RefUpdate.Result res = ru.update(revWalk); |
| if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) { |
| throw new IOException( |
| "cannot update " |
| + ru.getName() |
| + " in " |
| + repository.getDirectory() |
| + ": " |
| + ru.getResult()); |
| } |
| } |
| } |
| |
| private void updateReferenceWithNameChange( |
| Repository repository, |
| String currentRefName, |
| ObjectId currentObjectId, |
| String newRefName, |
| ObjectId targetObjectId, |
| Timestamp timestamp) |
| throws IOException { |
| BatchRefUpdate batchRefUpdate = repository.getRefDatabase().newBatchUpdate(); |
| batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), targetObjectId, newRefName)); |
| batchRefUpdate.addCommand( |
| new ReceiveCommand(currentObjectId, ObjectId.zeroId(), currentRefName)); |
| batchRefUpdate.setRefLogMessage("rebase edit", false); |
| batchRefUpdate.setRefLogIdent(getRefLogIdent(timestamp)); |
| try (RevWalk revWalk = new RevWalk(repository)) { |
| batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE); |
| } |
| for (ReceiveCommand cmd : batchRefUpdate.getCommands()) { |
| if (cmd.getResult() != ReceiveCommand.Result.OK) { |
| throw new IOException("failed: " + cmd); |
| } |
| } |
| } |
| |
| private PersonIdent getRefLogIdent(Timestamp timestamp) { |
| IdentifiedUser user = currentUser.get().asIdentifiedUser(); |
| return user.newRefLogIdent(timestamp, tz); |
| } |
| |
| private void reindex(Change change) { |
| indexer.index(change); |
| } |
| } |