blob: f80a4740c4410f19d39960d6ff31cefc223321bb [file] [log] [blame]
// 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.gerrit.common.TimeUtil;
import com.google.gerrit.common.errors.EmailException;
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.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeConflictException;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
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.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.util.List;
import java.util.TimeZone;
@Singleton
public class RebaseChange {
private final ChangeControl.GenericFactory changeControlFactory;
private final Provider<ReviewDb> db;
private final GitRepositoryManager gitManager;
private final TimeZone serverTimeZone;
private final MergeUtil.Factory mergeUtilFactory;
private final PatchSetInserter.Factory patchSetInserterFactory;
@Inject
RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
final Provider<ReviewDb> db,
@GerritPersonIdent final PersonIdent myIdent,
final GitRepositoryManager gitManager,
final MergeUtil.Factory mergeUtilFactory,
final PatchSetInserter.Factory patchSetInserterFactory) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.gitManager = gitManager;
this.serverTimeZone = myIdent.getTimeZone();
this.mergeUtilFactory = mergeUtilFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
}
/**
* 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 change the change to perform the rebase for
* @param patchSetId the id of the patch set
* @param uploader the user that creates the rebased patch set
* @param newBaseRev the commit that should be the new base
* @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(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader,
final String newBaseRev) throws NoSuchChangeException, EmailException, OrmException,
IOException, InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl changeControl =
changeControlFactory.validateFor(change, uploader);
if (!changeControl.canRebase()) {
throw new InvalidChangeOperationException(
"Cannot rebase: New patch sets are not allowed to be added to change: "
+ changeId.toString());
}
Repository git = null;
RevWalk rw = null;
ObjectInserter inserter = null;
try {
git = gitManager.openRepository(change.getProject());
rw = new RevWalk(git);
inserter = git.newObjectInserter();
String baseRev = newBaseRev;
if (baseRev == null) {
baseRev = findBaseRevision(patchSetId, db.get(),
change.getDest(), git, null, null, null);
}
ObjectId baseObjectId = git.resolve(baseRev);
if (baseObjectId == null) {
throw new InvalidChangeOperationException(
"Cannot rebase: Failed to resolve baseRev: " + baseRev);
}
final RevCommit baseCommit = rw.parseCommit(baseObjectId);
PersonIdent committerIdent =
uploader.newCommitterIdent(TimeUtil.nowTs(),
serverTimeZone);
rebase(git, rw, inserter, patchSetId, change,
uploader, baseCommit, mergeUtilFactory.create(
changeControl.getProjectControl().getProjectState(), true),
committerIdent, true, ValidatePolicy.GERRIT);
} catch (MergeConflictException 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. Parent 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();
}
for (PatchSet depPatchSet : depPatchSetList) {
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.getDest().equals(destBranch)) {
continue;
}
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();
}
break;
}
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 only done for the creation of
* the new patch set if `sendEmail` and `runHooks` are set to true.
*
* @param git the repository
* @param revWalk the RevWalk
* @param inserter the object inserter
* @param patchSetId the id of the patch set
* @param change 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
* @param committerIdent the committer's identity
* @param runHooks if hooks should be run for the new patch set
* @param validate if commit validation should be run for the new patch set
* @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 change, final IdentifiedUser uploader, final RevCommit baseCommit,
final MergeUtil mergeUtil, PersonIdent committerIdent,
boolean runHooks, ValidatePolicy validate)
throws NoSuchChangeException,
OrmException, IOException, InvalidChangeOperationException,
MergeConflictException {
if (!change.currentPatchSetId().equals(patchSetId)) {
throw new InvalidChangeOperationException("patch set is not current");
}
final PatchSet originalPatchSet = db.get().patchSets().get(patchSetId);
final RevCommit rebasedCommit;
ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
ObjectId newId = rebaseCommit(git, inserter, revWalk.parseCommit(oldId),
baseCommit, mergeUtil, committerIdent);
rebasedCommit = revWalk.parseCommit(newId);
final ChangeControl changeControl =
changeControlFactory.validateFor(change, uploader);
PatchSetInserter patchSetInserter = patchSetInserterFactory
.create(git, revWalk, changeControl, rebasedCommit)
.setValidatePolicy(validate)
.setDraft(originalPatchSet.isDraft())
.setUploader(uploader.getAccountId())
.setSendMail(false)
.setRunHooks(runHooks);
final PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
final ChangeMessage cmsg = new ChangeMessage(
new ChangeMessage.Key(change.getId(),
ChangeUtil.messageUUID(db.get())), uploader.getAccountId(),
TimeUtil.nowTs(), patchSetId);
cmsg.setMessage("Patch Set " + newPatchSetId.get()
+ ": Patch Set " + patchSetId.get() + " was rebased");
Change newChange = patchSetInserter
.setMessage(cmsg)
.insert();
return db.get().patchSets().get(newChange.currentPatchSetId());
}
/**
* Rebase 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 MergeConflictException the rebase failed due to a merge conflict.
* @throws IOException the merge failed for another reason.
*/
private ObjectId rebaseCommit(Repository git, ObjectInserter inserter,
RevCommit original, RevCommit base, MergeUtil mergeUtil,
PersonIdent committerIdent) throws MergeConflictException, IOException {
RevCommit parentCommit = original.getParent(0);
if (base.equals(parentCommit)) {
throw new IOException("Change is already up to date.");
}
ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter);
merger.setBase(parentCommit);
merger.merge(original, base);
if (merger.getResultTreeId() == null) {
throw new MergeConflictException(
"The change could not be rebased due to a conflict during merge.");
}
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(merger.getResultTreeId());
cb.setParentId(base);
cb.setAuthor(original.getAuthorIdent());
cb.setMessage(original.getFullMessage());
cb.setCommitter(committerIdent);
ObjectId objectId = inserter.insert(cb);
inserter.flush();
return objectId;
}
public boolean canRebase(RevisionResource r) {
Repository git;
try {
git = gitManager.openRepository(r.getChange().getProject());
} catch (RepositoryNotFoundException err) {
return false;
} catch (IOException err) {
return false;
}
try {
findBaseRevision(
r.getPatchSet().getId(),
db.get(),
r.getChange().getDest(),
git,
null,
null,
null);
return true;
} catch (IOException e) {
return false;
} catch (OrmException e) {
return false;
} finally {
git.close();
}
}
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();
}
}
}