| // Copyright (C) 2012 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.changedetail; |
| |
| import com.google.common.collect.Sets; |
| import com.google.gerrit.common.ChangeHookRunner; |
| import com.google.gerrit.common.data.LabelTypes; |
| import com.google.gerrit.common.errors.EmailException; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.Branch; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.Change.Status; |
| import com.google.gerrit.reviewdb.client.ChangeMessage; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.PatchSetAncestor; |
| import com.google.gerrit.reviewdb.client.PatchSetApproval; |
| import com.google.gerrit.reviewdb.client.PatchSetInfo; |
| import com.google.gerrit.reviewdb.client.RevId; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.ApprovalsUtil; |
| import com.google.gerrit.server.ChangeUtil; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.MergeUtil; |
| import com.google.gerrit.server.mail.RebasedPatchSetSender; |
| import com.google.gerrit.server.mail.ReplacePatchSetSender; |
| import com.google.gerrit.server.patch.PatchSetInfoFactory; |
| import com.google.gerrit.server.project.ChangeControl; |
| import com.google.gerrit.server.project.InvalidChangeOperationException; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gwtorm.server.AtomicUpdate; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.merge.ThreeWayMerger; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| |
| public class RebaseChange { |
| private final ChangeControl.Factory changeControlFactory; |
| private final PatchSetInfoFactory patchSetInfoFactory; |
| private final ReviewDb db; |
| private final GitRepositoryManager gitManager; |
| private final PersonIdent myIdent; |
| private final GitReferenceUpdated gitRefUpdated; |
| private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory; |
| private final ChangeHookRunner hooks; |
| private final ApprovalsUtil approvalsUtil; |
| private final MergeUtil.Factory mergeUtilFactory; |
| private final ProjectCache projectCache; |
| |
| @Inject |
| RebaseChange(final ChangeControl.Factory changeControlFactory, |
| final PatchSetInfoFactory patchSetInfoFactory, final ReviewDb db, |
| @GerritPersonIdent final PersonIdent myIdent, |
| final GitRepositoryManager gitManager, |
| final GitReferenceUpdated gitRefUpdated, |
| final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory, |
| final ChangeHookRunner hooks, final ApprovalsUtil approvalsUtil, |
| final MergeUtil.Factory mergeUtilFactory, |
| final ProjectCache projectCache) { |
| this.changeControlFactory = changeControlFactory; |
| this.patchSetInfoFactory = patchSetInfoFactory; |
| this.db = db; |
| this.gitManager = gitManager; |
| this.myIdent = myIdent; |
| this.gitRefUpdated = gitRefUpdated; |
| this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory; |
| this.hooks = hooks; |
| this.approvalsUtil = approvalsUtil; |
| this.mergeUtilFactory = mergeUtilFactory; |
| this.projectCache = projectCache; |
| } |
| |
| /** |
| * Rebases the change of the given patch set. |
| * |
| * It is verified that the current user is allowed to do the rebase. |
| * |
| * If the patch set has no dependency to an open change, then the change is |
| * rebased on the tip of the destination branch. |
| * |
| * If the patch set depends on an open change, it is rebased on the latest |
| * patch set of this change. |
| * |
| * The rebased commit is added as new patch set to the change. |
| * |
| * E-mail notification and triggering of hooks happens for the creation of the |
| * new patch set. |
| * |
| * @param patchSetId the id of the patch set |
| * @param uploader the user that creates the rebased patch set |
| * @throws NoSuchChangeException thrown if the change to which the patch set |
| * belongs does not exist or is not visible to the user |
| * @throws EmailException thrown if sending the e-mail to notify about the new |
| * patch set fails |
| * @throws OrmException thrown in case accessing the database fails |
| * @throws IOException thrown if rebase is not possible or not needed |
| * @throws InvalidChangeOperationException thrown if rebase is not allowed |
| */ |
| public void rebase(final PatchSet.Id patchSetId, final Account.Id uploader) |
| throws NoSuchChangeException, EmailException, OrmException, IOException, |
| InvalidChangeOperationException { |
| final Change.Id changeId = patchSetId.getParentKey(); |
| final ChangeControl changeControl = |
| changeControlFactory.validateFor(changeId); |
| if (!changeControl.canRebase()) { |
| throw new InvalidChangeOperationException( |
| "Cannot rebase: New patch sets are not allowed to be added to change: " |
| + changeId.toString()); |
| } |
| final Change change = changeControl.getChange(); |
| Repository git = null; |
| RevWalk rw = null; |
| ObjectInserter inserter = null; |
| try { |
| git = gitManager.openRepository(change.getProject()); |
| rw = new RevWalk(git); |
| inserter = git.newObjectInserter(); |
| |
| final List<PatchSetApproval> oldPatchSetApprovals = |
| db.patchSetApprovals().byChange(change.getId()).toList(); |
| |
| final String baseRev = findBaseRevision(patchSetId, db, |
| change.getDest(), git, null, null, null); |
| final RevCommit baseCommit = |
| rw.parseCommit(ObjectId.fromString(baseRev)); |
| |
| final PatchSet newPatchSet = |
| rebase(git, rw, inserter, patchSetId, change, uploader, baseCommit, |
| mergeUtilFactory.create( |
| changeControl.getProjectControl().getProjectState(), true)); |
| |
| final Set<Account.Id> oldReviewers = Sets.newHashSet(); |
| final Set<Account.Id> oldCC = Sets.newHashSet(); |
| for (PatchSetApproval a : oldPatchSetApprovals) { |
| if (a.getValue() != 0) { |
| oldReviewers.add(a.getAccountId()); |
| } else { |
| oldCC.add(a.getAccountId()); |
| } |
| } |
| final ReplacePatchSetSender cm = |
| rebasedPatchSetSenderFactory.create(change); |
| cm.setFrom(uploader); |
| cm.setPatchSet(newPatchSet); |
| cm.addReviewers(oldReviewers); |
| cm.addExtraCC(oldCC); |
| cm.send(); |
| |
| hooks.doPatchsetCreatedHook(change, newPatchSet, db); |
| } catch (PathConflictException e) { |
| throw new IOException(e.getMessage()); |
| } finally { |
| if (inserter != null) { |
| inserter.release(); |
| } |
| if (rw != null) { |
| rw.release(); |
| } |
| if (git != null) { |
| git.close(); |
| } |
| } |
| } |
| |
| /** |
| * Finds the revision of commit on which the given patch set should be based. |
| * |
| * @param patchSetId the id of the patch set for which the new base commit |
| * should be found |
| * @param db the ReviewDb |
| * @param destBranch the destination branch |
| * @param git the repository |
| * @param patchSetAncestors the original PatchSetAncestor of the given patch |
| * set that should be based |
| * @param depPatchSetList the original patch set list on which the rebased |
| * patch set depends |
| * @param depChangeList the original change list on whose patch set the |
| * rebased patch set depends |
| * @return the revision of commit on which the given patch set should be based |
| * @throws IOException thrown if rebase is not possible or not needed |
| * @throws OrmException thrown in case accessing the database fails |
| */ |
| private static String findBaseRevision(final PatchSet.Id patchSetId, |
| final ReviewDb db, final Branch.NameKey destBranch, final Repository git, |
| List<PatchSetAncestor> patchSetAncestors, List<PatchSet> depPatchSetList, |
| List<Change> depChangeList) throws IOException, OrmException { |
| |
| String baseRev = null; |
| |
| if (patchSetAncestors == null) { |
| patchSetAncestors = |
| db.patchSetAncestors().ancestorsOf(patchSetId).toList(); |
| } |
| |
| if (patchSetAncestors.size() > 1) { |
| throw new IOException( |
| "Cannot rebase a change with multiple parents. Parents commits: " |
| + patchSetAncestors.toString()); |
| } |
| if (patchSetAncestors.size() == 0) { |
| throw new IOException( |
| "Cannot rebase a change without any parents (is this the initial commit?)."); |
| } |
| |
| RevId ancestorRev = patchSetAncestors.get(0).getAncestorRevision(); |
| if (depPatchSetList == null || depPatchSetList.size() != 1 || |
| !depPatchSetList.get(0).getRevision().equals(ancestorRev)) { |
| depPatchSetList = db.patchSets().byRevision(ancestorRev).toList(); |
| } |
| |
| if (!depPatchSetList.isEmpty()) { |
| PatchSet depPatchSet = depPatchSetList.get(0); |
| |
| Change.Id depChangeId = depPatchSet.getId().getParentKey(); |
| Change depChange; |
| if (depChangeList == null || depChangeList.size() != 1 || |
| !depChangeList.get(0).getId().equals(depChangeId)) { |
| depChange = db.changes().get(depChangeId); |
| } else { |
| depChange = depChangeList.get(0); |
| } |
| |
| if (depChange.getStatus() == Status.ABANDONED) { |
| throw new IOException("Cannot rebase a change with an abandoned parent: " |
| + depChange.getKey().toString()); |
| } |
| |
| if (depChange.getStatus().isOpen()) { |
| if (depPatchSet.getId().equals(depChange.currentPatchSetId())) { |
| throw new IOException( |
| "Change is already based on the latest patch set of the dependent change."); |
| } |
| PatchSet latestDepPatchSet = |
| db.patchSets().get(depChange.currentPatchSetId()); |
| baseRev = latestDepPatchSet.getRevision().get(); |
| } |
| } |
| |
| if (baseRev == null) { |
| // We are dependent on a merged PatchSet or have no PatchSet |
| // dependencies at all. |
| Ref destRef = git.getRef(destBranch.get()); |
| if (destRef == null) { |
| throw new IOException( |
| "The destination branch does not exist: " |
| + destBranch.get()); |
| } |
| baseRev = destRef.getObjectId().getName(); |
| if (baseRev.equals(ancestorRev.get())) { |
| throw new IOException("Change is already up to date."); |
| } |
| } |
| return baseRev; |
| } |
| |
| /** |
| * Rebases the change of the given patch set on the given base commit. |
| * |
| * The rebased commit is added as new patch set to the change. |
| * |
| * E-mail notification and triggering of hooks is NOT done for the creation of |
| * the new patch set. |
| * |
| * @param git the repository |
| * @param revWalk the RevWalk |
| * @param inserter the object inserter |
| * @param patchSetId the id of the patch set |
| * @param chg the change that should be rebased |
| * @param uploader the user that creates the rebased patch set |
| * @param baseCommit the commit that should be the new base |
| * @param mergeUtil merge utilities for the destination project |
| * @return the new patch set which is based on the given base commit |
| * @throws NoSuchChangeException thrown if the change to which the patch set |
| * belongs does not exist or is not visible to the user |
| * @throws OrmException thrown in case accessing the database fails |
| * @throws IOException thrown if rebase is not possible or not needed |
| * @throws InvalidChangeOperationException thrown if rebase is not allowed |
| */ |
| public PatchSet rebase(final Repository git, final RevWalk revWalk, |
| final ObjectInserter inserter, final PatchSet.Id patchSetId, |
| final Change chg, final Account.Id uploader, final RevCommit baseCommit, |
| final MergeUtil mergeUtil) throws NoSuchChangeException, |
| OrmException, IOException, InvalidChangeOperationException, |
| PathConflictException { |
| Change change = chg; |
| if (!chg.currentPatchSetId().equals(patchSetId)) { |
| throw new InvalidChangeOperationException("patch set is not current"); |
| } |
| final PatchSet originalPatchSet = db.patchSets().get(patchSetId); |
| |
| final RevCommit rebasedCommit; |
| ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get()); |
| ObjectId newId = rebaseCommit(git, inserter, revWalk.parseCommit(oldId), |
| baseCommit, mergeUtil, myIdent); |
| |
| rebasedCommit = revWalk.parseCommit(newId); |
| |
| PatchSet.Id id = ChangeUtil.nextPatchSetId(git, change.currentPatchSetId()); |
| final PatchSet newPatchSet = new PatchSet(id); |
| newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis())); |
| newPatchSet.setUploader(uploader); |
| newPatchSet.setRevision(new RevId(rebasedCommit.name())); |
| newPatchSet.setDraft(originalPatchSet.isDraft()); |
| |
| final PatchSetInfo info = |
| patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId()); |
| |
| final RefUpdate ru = git.updateRef(newPatchSet.getRefName()); |
| ru.setExpectedOldObjectId(ObjectId.zeroId()); |
| ru.setNewObjectId(rebasedCommit); |
| ru.disableRefLog(); |
| if (ru.update(revWalk) != RefUpdate.Result.NEW) { |
| throw new IOException(String.format("Failed to create ref %s in %s: %s", |
| newPatchSet.getRefName(), change.getDest().getParentKey().get(), |
| ru.getResult())); |
| } |
| gitRefUpdated.fire(change.getProject(), ru); |
| |
| db.changes().beginTransaction(change.getId()); |
| try { |
| Change updatedChange = db.changes().get(change.getId()); |
| if (updatedChange != null && change.getStatus().isOpen()) { |
| change = updatedChange; |
| } else { |
| throw new InvalidChangeOperationException(String.format( |
| "Change %s is closed", change.getId())); |
| } |
| |
| ChangeUtil.insertAncestors(db, newPatchSet.getId(), rebasedCommit); |
| db.patchSets().insert(Collections.singleton(newPatchSet)); |
| updatedChange = |
| db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() { |
| @Override |
| public Change update(Change change) { |
| if (change.getStatus().isClosed()) { |
| return null; |
| } |
| if (!change.currentPatchSetId().equals(patchSetId)) { |
| return null; |
| } |
| if (change.getStatus() != Change.Status.DRAFT) { |
| change.setStatus(Change.Status.NEW); |
| } |
| change.setLastSha1MergeTested(null); |
| change.setCurrentPatchSet(info); |
| ChangeUtil.updated(change); |
| return change; |
| } |
| }); |
| if (updatedChange != null) { |
| change = updatedChange; |
| } else { |
| throw new InvalidChangeOperationException(String.format( |
| "Change %s was modified", change.getId())); |
| } |
| |
| final LabelTypes labelTypes = |
| projectCache.get(change.getProject()).getLabelTypes(); |
| approvalsUtil.copyVetosToPatchSet(db, labelTypes, |
| change.currentPatchSetId()); |
| |
| final ChangeMessage cmsg = |
| new ChangeMessage(new ChangeMessage.Key(change.getId(), |
| ChangeUtil.messageUUID(db)), uploader, patchSetId); |
| cmsg.setMessage("Patch Set " + change.currentPatchSetId().get() |
| + ": Patch Set " + patchSetId.get() + " was rebased"); |
| db.changeMessages().insert(Collections.singleton(cmsg)); |
| db.commit(); |
| } finally { |
| db.rollback(); |
| } |
| |
| return newPatchSet; |
| } |
| |
| /** |
| * Rebases a commit. |
| * |
| * @param git repository to find commits in |
| * @param inserter inserter to handle new trees and blobs |
| * @param original The commit to rebase |
| * @param base Base to rebase against |
| * @param mergeUtil merge utilities for the destination project |
| * @param committerIdent committer identity |
| * @return the id of the rebased commit |
| * @throws IOException Merged failed |
| * @throws PathConflictException the rebase failed due to a path conflict |
| */ |
| private ObjectId rebaseCommit(final Repository git, |
| final ObjectInserter inserter, final RevCommit original, |
| final RevCommit base, final MergeUtil mergeUtil, |
| final PersonIdent committerIdent) throws IOException, |
| PathConflictException { |
| |
| final RevCommit parentCommit = original.getParent(0); |
| |
| if (base.equals(parentCommit)) { |
| throw new IOException("Change is already up to date."); |
| } |
| |
| final ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter); |
| merger.setBase(parentCommit); |
| merger.merge(original, base); |
| |
| if (merger.getResultTreeId() == null) { |
| throw new PathConflictException( |
| "The change could not be rebased due to a path conflict during merge."); |
| } |
| |
| final CommitBuilder cb = new CommitBuilder(); |
| cb.setTreeId(merger.getResultTreeId()); |
| cb.setParentId(base); |
| cb.setAuthor(original.getAuthorIdent()); |
| cb.setMessage(original.getFullMessage()); |
| cb.setCommitter(committerIdent); |
| final ObjectId objectId = inserter.insert(cb); |
| inserter.flush(); |
| return objectId; |
| } |
| |
| public static boolean canDoRebase(final ReviewDb db, |
| final Change change, final GitRepositoryManager gitManager, |
| List<PatchSetAncestor> patchSetAncestors, |
| List<PatchSet> depPatchSetList, List<Change> depChangeList) |
| throws OrmException, RepositoryNotFoundException, IOException { |
| |
| final Repository git = gitManager.openRepository(change.getProject()); |
| |
| try { |
| // If no exception is thrown, then we can do a rebase. |
| findBaseRevision(change.currentPatchSetId(), db, change.getDest(), git, |
| patchSetAncestors, depPatchSetList, depChangeList); |
| return true; |
| } catch (IOException e) { |
| return false; |
| } finally { |
| git.close(); |
| } |
| } |
| } |