| // 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.entities.BooleanProjectConfig; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.RefNames; | |
| 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.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 = currentPatchSet.commitId(); | |
| 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.id())); | |
| } | |
| 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, Project.NameKey project, 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); | |
| ChangeUtil.ensureChangeIdIsCorrect( | |
| projectCache.checkedGet(project).is(BooleanProjectConfig.REQUIRE_CHANGE_ID), | |
| notes.getChange().getKey().get(), | |
| newCommitMessage); | |
| 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.isEqual(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().id(), patchSet.id())); | |
| } | |
| } else { | |
| PatchSet.Id patchSetId = patchSet.id(); | |
| 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.id().equals(patchSet.id()); | |
| } | |
| private static RevCommit lookupCommit(Repository repository, PatchSet patchSet) | |
| throws IOException { | |
| ObjectId patchSetCommitId = patchSet.commitId(); | |
| 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.isEqual(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 = basePatchSet.commitId(); | |
| 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 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.id()); | |
| } | |
| 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); | |
| } | |
| } |