blob: 20dbfb335eff201239b609929091ccaa7d243b76 [file] [log] [blame]
// Copyright (C) 2015 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.change;
import static com.google.common.base.Preconditions.checkState;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.Context;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.validators.CommitValidators;
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.ProjectState;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
public class RebaseChangeOp extends BatchUpdate.Op {
public interface Factory {
RebaseChangeOp create(ChangeControl ctl, PatchSet originalPatchSet,
@Nullable String baseCommitish);
}
private final PatchSetInserter.Factory patchSetInserterFactory;
private final MergeUtil.Factory mergeUtilFactory;
private final RebaseUtil rebaseUtil;
private final ChangeResource.Factory changeResourceFactory;
private final ChangeControl ctl;
private final PatchSet originalPatchSet;
private String baseCommitish;
private PersonIdent committerIdent;
private boolean fireRevisionCreated = true;
private CommitValidators.Policy validate;
private boolean checkAddPatchSetPermission = true;
private boolean forceContentMerge;
private boolean copyApprovals = true;
private RevCommit rebasedCommit;
private PatchSet.Id rebasedPatchSetId;
private PatchSetInserter patchSetInserter;
private PatchSet rebasedPatchSet;
@AssistedInject
RebaseChangeOp(
PatchSetInserter.Factory patchSetInserterFactory,
MergeUtil.Factory mergeUtilFactory,
RebaseUtil rebaseUtil,
ChangeResource.Factory changeResourceFactory,
@Assisted ChangeControl ctl,
@Assisted PatchSet originalPatchSet,
@Assisted @Nullable String baseCommitish) {
this.patchSetInserterFactory = patchSetInserterFactory;
this.mergeUtilFactory = mergeUtilFactory;
this.rebaseUtil = rebaseUtil;
this.changeResourceFactory = changeResourceFactory;
this.ctl = ctl;
this.originalPatchSet = originalPatchSet;
this.baseCommitish = baseCommitish;
}
public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
this.committerIdent = committerIdent;
return this;
}
public RebaseChangeOp setValidatePolicy(CommitValidators.Policy validate) {
this.validate = validate;
return this;
}
public RebaseChangeOp setCheckAddPatchSetPermission(
boolean checkAddPatchSetPermission) {
this.checkAddPatchSetPermission = checkAddPatchSetPermission;
return this;
}
public RebaseChangeOp setFireRevisionCreated(boolean fireRevisionCreated) {
this.fireRevisionCreated = fireRevisionCreated;
return this;
}
public RebaseChangeOp setForceContentMerge(boolean forceContentMerge) {
this.forceContentMerge = forceContentMerge;
return this;
}
public RebaseChangeOp setCopyApprovals(boolean copyApprovals) {
this.copyApprovals = copyApprovals;
return this;
}
@Override
public void updateRepo(RepoContext ctx) throws MergeConflictException,
InvalidChangeOperationException, RestApiException, IOException,
OrmException, NoSuchChangeException {
// Ok that originalPatchSet was not read in a transaction, since we just
// need its revision.
RevId oldRev = originalPatchSet.getRevision();
RevWalk rw = ctx.getRevWalk();
RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get()));
rw.parseBody(original);
RevCommit baseCommit;
if (baseCommitish != null) {
baseCommit = rw.parseCommit(ctx.getRepository().resolve(baseCommitish));
} else {
baseCommit = rw.parseCommit(rebaseUtil.findBaseRevision(
originalPatchSet, ctl.getChange().getDest(),
ctx.getRepository(), ctx.getRevWalk()));
}
rebasedCommit = rebaseCommit(ctx, original, baseCommit);
RevId baseRevId = new RevId((baseCommitish != null) ? baseCommitish
: ObjectId.toString(baseCommit.getId()));
Base base = rebaseUtil.parseBase(
new RevisionResource(
changeResourceFactory.create(ctl), originalPatchSet),
baseRevId.get());
rebasedPatchSetId = ChangeUtil.nextPatchSetId(
ctx.getRepository(), ctl.getChange().currentPatchSetId());
patchSetInserter = patchSetInserterFactory
.create(ctl, rebasedPatchSetId, rebasedCommit)
.setDraft(originalPatchSet.isDraft())
.setSendMail(false)
.setFireRevisionCreated(fireRevisionCreated)
.setCopyApprovals(copyApprovals)
.setCheckAddPatchSetPermission(checkAddPatchSetPermission)
.setMessage(
"Patch Set " + rebasedPatchSetId.get()
+ ": Patch Set " + originalPatchSet.getId().get() + " was rebased");
if (base != null) {
patchSetInserter.setGroups(base.patchSet().getGroups());
}
if (validate != null) {
patchSetInserter.setValidatePolicy(validate);
}
patchSetInserter.updateRepo(ctx);
}
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceConflictException, OrmException, IOException {
boolean ret = patchSetInserter.updateChange(ctx);
rebasedPatchSet = patchSetInserter.getPatchSet();
return ret;
}
@Override
public void postUpdate(Context ctx) throws OrmException {
patchSetInserter.postUpdate(ctx);
}
public RevCommit getRebasedCommit() {
checkState(rebasedCommit != null,
"getRebasedCommit() only valid after updateRepo");
return rebasedCommit;
}
public PatchSet.Id getPatchSetId() {
checkState(rebasedPatchSetId != null,
"getPatchSetId() only valid after updateRepo");
return rebasedPatchSetId;
}
public PatchSet getPatchSet() {
checkState(rebasedPatchSet != null,
"getPatchSet() only valid after executing update");
return rebasedPatchSet;
}
private MergeUtil newMergeUtil() {
ProjectState project = ctl.getProjectControl().getProjectState();
return forceContentMerge
? mergeUtilFactory.create(project, true)
: mergeUtilFactory.create(project);
}
/**
* Rebase a commit.
*
* @param ctx repo context.
* @param original the commit to rebase.
* @param base base to rebase against.
* @return the rebased commit.
* @throws MergeConflictException the rebase failed due to a merge conflict.
* @throws IOException the merge failed for another reason.
*/
private RevCommit rebaseCommit(RepoContext ctx, RevCommit original,
ObjectId base) throws ResourceConflictException, MergeConflictException,
IOException {
RevCommit parentCommit = original.getParent(0);
if (base.equals(parentCommit)) {
throw new ResourceConflictException("Change is already up to date.");
}
ThreeWayMerger merger = newMergeUtil().newThreeWayMerger(
ctx.getRepository(), ctx.getInserter());
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());
if (committerIdent != null) {
cb.setCommitter(committerIdent);
} else {
cb.setCommitter(ctx.getIdentifiedUser()
.newCommitterIdent(ctx.getWhen(), ctx.getTimeZone()));
}
ObjectId objectId = ctx.getInserter().insert(cb);
ctx.getInserter().flush();
return ctx.getRevWalk().parseCommit(objectId);
}
}