com.google.gerrit.server.change.RebaseChangeOp Maven / Gradle / Ivy
// 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.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Objects.requireNonNull;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.BadRequestException;
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.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.MergeUtilFactory;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.PostUpdateContext;
import com.google.gerrit.server.update.RepoContext;
import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.merge.MergeResult;
import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* BatchUpdate operation that rebases a change.
*
* Can only be executed in a {@link com.google.gerrit.server.update.BatchUpdate} set has a {@link
* CodeReviewRevWalk} set as {@link RevWalk} (set via {@link
* com.google.gerrit.server.update.BatchUpdate#setRepository(org.eclipse.jgit.lib.Repository,
* RevWalk, org.eclipse.jgit.lib.ObjectInserter)}).
*/
public class RebaseChangeOp implements BatchUpdateOp {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public interface Factory {
RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, ObjectId baseCommitId);
RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, Change.Id baseChangeId);
}
private final PatchSetInserter.Factory patchSetInserterFactory;
private final MergeUtilFactory mergeUtilFactory;
private final RebaseUtil rebaseUtil;
private final ChangeResource.Factory changeResourceFactory;
private final ChangeNotes.Factory notesFactory;
private final ChangeNotes notes;
private final PatchSet originalPatchSet;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final ProjectCache projectCache;
private final Project.NameKey projectName;
private ObjectId baseCommitId;
private Change.Id baseChangeId;
private PersonIdent committerIdent;
private boolean fireRevisionCreated = true;
private boolean validate = true;
private boolean checkAddPatchSetPermission = true;
private boolean forceContentMerge;
private boolean allowConflicts;
private boolean detailedCommitMessage;
private boolean postMessage = true;
private boolean sendEmail = true;
private boolean storeCopiedVotes = true;
private boolean matchAuthorToCommitterDate = false;
private ImmutableListMultimap validationOptions = ImmutableListMultimap.of();
private String mergeStrategy;
private boolean verifyNeedsRebase = true;
private CodeReviewCommit rebasedCommit;
private PatchSet.Id rebasedPatchSetId;
private PatchSetInserter patchSetInserter;
private PatchSet rebasedPatchSet;
@AssistedInject
RebaseChangeOp(
PatchSetInserter.Factory patchSetInserterFactory,
MergeUtilFactory mergeUtilFactory,
RebaseUtil rebaseUtil,
ChangeResource.Factory changeResourceFactory,
ChangeNotes.Factory notesFactory,
GenericFactory identifiedUserFactory,
ProjectCache projectCache,
@Assisted ChangeNotes notes,
@Assisted PatchSet originalPatchSet,
@Assisted ObjectId baseCommitId) {
this(
patchSetInserterFactory,
mergeUtilFactory,
rebaseUtil,
changeResourceFactory,
notesFactory,
identifiedUserFactory,
projectCache,
notes,
originalPatchSet);
this.baseCommitId = baseCommitId;
this.baseChangeId = null;
}
@AssistedInject
RebaseChangeOp(
PatchSetInserter.Factory patchSetInserterFactory,
MergeUtilFactory mergeUtilFactory,
RebaseUtil rebaseUtil,
ChangeResource.Factory changeResourceFactory,
ChangeNotes.Factory notesFactory,
GenericFactory identifiedUserFactory,
ProjectCache projectCache,
@Assisted ChangeNotes notes,
@Assisted PatchSet originalPatchSet,
@Assisted Change.Id baseChangeId) {
this(
patchSetInserterFactory,
mergeUtilFactory,
rebaseUtil,
changeResourceFactory,
notesFactory,
identifiedUserFactory,
projectCache,
notes,
originalPatchSet);
this.baseChangeId = baseChangeId;
this.baseCommitId = null;
}
private RebaseChangeOp(
PatchSetInserter.Factory patchSetInserterFactory,
MergeUtilFactory mergeUtilFactory,
RebaseUtil rebaseUtil,
ChangeResource.Factory changeResourceFactory,
ChangeNotes.Factory notesFactory,
GenericFactory identifiedUserFactory,
ProjectCache projectCache,
ChangeNotes notes,
PatchSet originalPatchSet) {
this.patchSetInserterFactory = patchSetInserterFactory;
this.mergeUtilFactory = mergeUtilFactory;
this.rebaseUtil = rebaseUtil;
this.changeResourceFactory = changeResourceFactory;
this.notesFactory = notesFactory;
this.identifiedUserFactory = identifiedUserFactory;
this.projectCache = projectCache;
this.notes = notes;
this.projectName = notes.getProjectName();
this.originalPatchSet = originalPatchSet;
}
@CanIgnoreReturnValue
public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
this.committerIdent = committerIdent;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setValidate(boolean validate) {
this.validate = validate;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setCheckAddPatchSetPermission(boolean checkAddPatchSetPermission) {
this.checkAddPatchSetPermission = checkAddPatchSetPermission;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setFireRevisionCreated(boolean fireRevisionCreated) {
this.fireRevisionCreated = fireRevisionCreated;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setForceContentMerge(boolean forceContentMerge) {
this.forceContentMerge = forceContentMerge;
return this;
}
/**
* Allows the rebase to succeed if there are conflicts.
*
* This setting requires that {@link #forceContentMerge} is set {@code true}. If {@link
* #forceContentMerge} is {@code false} this setting has no effect.
*
* @see #setForceContentMerge(boolean)
*/
@CanIgnoreReturnValue
public RebaseChangeOp setAllowConflicts(boolean allowConflicts) {
this.allowConflicts = allowConflicts;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setDetailedCommitMessage(boolean detailedCommitMessage) {
this.detailedCommitMessage = detailedCommitMessage;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setPostMessage(boolean postMessage) {
this.postMessage = postMessage;
return this;
}
/**
* We always want to store copied votes except when the change is getting submitted and a new
* patch-set is created on submit (using submit strategies such as "REBASE_ALWAYS"). In such
* cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
* should not also store the copied votes.
*/
@CanIgnoreReturnValue
public RebaseChangeOp setStoreCopiedVotes(boolean storeCopiedVotes) {
this.storeCopiedVotes = storeCopiedVotes;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setSendEmail(boolean sendEmail) {
this.sendEmail = sendEmail;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setMatchAuthorToCommitterDate(boolean matchAuthorToCommitterDate) {
this.matchAuthorToCommitterDate = matchAuthorToCommitterDate;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setValidationOptions(
ImmutableListMultimap validationOptions) {
requireNonNull(validationOptions, "validationOptions may not be null");
this.validationOptions = validationOptions;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setMergeStrategy(String strategy) {
this.mergeStrategy = strategy;
return this;
}
@CanIgnoreReturnValue
public RebaseChangeOp setVerifyNeedsRebase(boolean verifyNeedsRebase) {
this.verifyNeedsRebase = verifyNeedsRebase;
return this;
}
@Override
public void updateRepo(RepoContext ctx)
throws InvalidChangeOperationException, RestApiException, IOException, NoSuchChangeException,
PermissionBackendException, DiffNotAvailableException {
// Ok that originalPatchSet was not read in a transaction, since we just
// need its revision.
RevWalk rw = ctx.getRevWalk();
RevCommit original = rw.parseCommit(originalPatchSet.commitId());
rw.parseBody(original);
RevCommit baseCommit;
if (baseCommitId != null && baseChangeId == null) {
baseCommit = rw.parseCommit(baseCommitId);
} else if (baseChangeId != null) {
baseCommit =
PatchSetUtil.getCurrentRevCommitIncludingPending(ctx, notesFactory, baseChangeId);
} else {
throw new IllegalStateException(
"Exactly one of base commit and base change must be provided.");
}
CurrentUser changeOwner = identifiedUserFactory.create(notes.getChange().getOwner());
String newCommitMessage;
if (detailedCommitMessage) {
rw.parseBody(baseCommit);
newCommitMessage =
newMergeUtil()
.createCommitMessageOnSubmit(original, baseCommit, notes, originalPatchSet.id());
} else {
newCommitMessage = original.getFullMessage();
}
rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage, notes.getChangeId());
Base base =
rebaseUtil.parseBase(
new RevisionResource(
changeResourceFactory.create(notes, changeOwner), originalPatchSet),
baseCommit.getName());
rebasedPatchSetId =
ChangeUtil.nextPatchSetIdFromChangeRefs(
ctx.getRepoView().getRefs(originalPatchSet.id().changeId().toRefPrefix()).keySet(),
notes.getChange().currentPatchSetId());
patchSetInserter =
patchSetInserterFactory
.create(notes, rebasedPatchSetId, rebasedCommit)
.setDescription("Rebase")
.setFireRevisionCreated(fireRevisionCreated)
.setCheckAddPatchSetPermission(checkAddPatchSetPermission)
.setValidate(validate)
.setSendEmail(sendEmail)
// The votes are automatically copied and they don't count as copied votes. See
// method's javadoc.
.setStoreCopiedVotes(storeCopiedVotes);
if (!rebasedCommit.getFilesWithGitConflicts().isEmpty()
&& !notes.getChange().isWorkInProgress()) {
patchSetInserter.setWorkInProgress(true);
}
patchSetInserter.setValidationOptions(validationOptions);
if (postMessage) {
patchSetInserter.setMessage(
messageForRebasedChange(
ctx.getIdentifiedUser(), rebasedPatchSetId, originalPatchSet.id(), rebasedCommit));
}
if (base != null && !base.notes().getChange().isMerged()) {
if (!base.notes().getChange().isMerged()) {
// Add to end of relation chain for open base change.
patchSetInserter.setGroups(base.patchSet().groups());
} else {
// If the base is merged, start a new relation chain.
patchSetInserter.setGroups(GroupCollector.getDefaultGroups(rebasedCommit));
}
}
logger.atFine().log(
"flushing inserter %s", ctx.getRevWalk().getObjectReader().getCreatedFromInserter());
ctx.getRevWalk().getObjectReader().getCreatedFromInserter().flush();
patchSetInserter.updateRepo(ctx);
}
private static String messageForRebasedChange(
IdentifiedUser user,
PatchSet.Id rebasePatchSetId,
PatchSet.Id originalPatchSetId,
CodeReviewCommit commit) {
StringBuilder stringBuilder =
new StringBuilder(
String.format(
"Patch Set %d: Patch Set %d was rebased",
rebasePatchSetId.get(), originalPatchSetId.get()));
if (user.isImpersonating()) {
stringBuilder.append(
String.format(
" on behalf of %s", AccountTemplateUtil.getAccountTemplate(user.getAccountId())));
}
if (!commit.getFilesWithGitConflicts().isEmpty()) {
stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
commit.getFilesWithGitConflicts().stream()
.sorted()
.forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
}
return stringBuilder.toString();
}
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceConflictException, IOException, BadRequestException {
boolean ret = patchSetInserter.updateChange(ctx);
rebasedPatchSet = patchSetInserter.getPatchSet();
return ret;
}
@Override
public void postUpdate(PostUpdateContext ctx) {
patchSetInserter.postUpdate(ctx);
}
public CodeReviewCommit getRebasedCommit() {
checkState(rebasedCommit != null, "getRebasedCommit() only valid after updateRepo");
return rebasedCommit;
}
public PatchSet getOriginalPatchSet() {
return originalPatchSet;
}
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 = projectCache.get(projectName).orElseThrow(illegalState(projectName));
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 CodeReviewCommit rebaseCommit(
RepoContext ctx,
RevCommit original,
ObjectId base,
String commitMessage,
Change.Id originalChangeId)
throws ResourceConflictException, IOException {
RevCommit parentCommit = original.getParent(0);
if (verifyNeedsRebase && base.equals(parentCommit)) {
throw new ResourceConflictException("Change is already up to date.");
}
MergeUtil mergeUtil = newMergeUtil();
String strategy =
firstNonNull(Strings.emptyToNull(mergeStrategy), mergeUtil.mergeStrategyName());
Merger merger = MergeUtil.newMerger(ctx.getInserter(), ctx.getRepoView().getConfig(), strategy);
if (merger instanceof ThreeWayMerger) {
((ThreeWayMerger) merger).setBase(parentCommit);
}
DirCache dc = DirCache.newInCore();
if (allowConflicts && merger instanceof ResolveMerger) {
// The DirCache must be set on ResolveMerger before calling
// ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
((ResolveMerger) merger).setDirCache(dc);
}
boolean success = merger.merge(original, base);
ObjectId tree;
ImmutableSet filesWithGitConflicts;
if (success) {
filesWithGitConflicts = null;
tree = merger.getResultTreeId();
logger.atFine().log(
"tree of rebased commit: %s (no conflicts, inserter: %s)",
tree.name(), merger.getObjectInserter());
} else {
List conflicts = ImmutableList.of();
Map failed = ImmutableMap.of();
if (merger instanceof ResolveMerger) {
conflicts = ((ResolveMerger) merger).getUnmergedPaths();
failed = ((ResolveMerger) merger).getFailingPaths();
}
if (merger.getResultTreeId() != null) {
// Merging with conflicts below uses the same DirCache instance that has been used by the
// Merger to attempt the merge without conflicts.
//
// The Merger uses the DirCache to do the updates, and in particular to write the result
// tree. DirCache caches a single DirCacheTree instance that is used to write the result
// tree, but it writes the result tree only if there were no conflicts.
//
// Merging with conflicts uses the same DirCache instance to write the tree with conflicts
// that has been used by the Merger. This means if the Merger unexpectedly wrote a result
// tree although there had been conflicts, then merging with conflicts uses the same
// DirCacheTree instance to write the tree with conflicts. However DirCacheTree#writeTree
// writes a tree only once and then that tree is cached. Further invocations of
// DirCacheTree#writeTree have no effect and return the previously created tree. This means
// merging with conflicts can only successfully create the tree with conflicts if the Merger
// didn't write a result tree yet. Hence this is checked here and we log a warning if the
// result tree was already written.
logger.atWarning().log(
"result tree has already been written: %s (merger: %s, conflicts: %s, failed: %s)",
merger, merger.getResultTreeId().name(), conflicts, failed);
}
if (!allowConflicts || !(merger instanceof ResolveMerger)) {
throw new MergeConflictException(
String.format(
"Change %s could not be rebased due to a conflict during merge.\n\n%s",
originalChangeId.toString(), MergeUtil.createConflictMessage(conflicts)));
}
Map> mergeResults =
((ResolveMerger) merger).getMergeResults();
filesWithGitConflicts =
mergeResults.entrySet().stream()
.filter(e -> e.getValue().containsConflicts())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
logger.atFine().log("rebasing with conflicts");
tree =
MergeUtil.mergeWithConflicts(
ctx.getRevWalk(),
ctx.getInserter(),
dc,
"PATCH SET",
original,
"BASE",
ctx.getRevWalk().parseCommit(base),
mergeResults);
logger.atFine().log(
"tree of rebased commit: %s (with conflicts, inserter: %s)",
tree.name(), ctx.getInserter());
}
List parents = new ArrayList<>();
parents.add(base);
if (original.getParentCount() > 1) {
// If a merge commit is rebased add all other parents (parent 2 to N).
for (int parent = 1; parent < original.getParentCount(); parent++) {
parents.add(original.getParent(parent));
}
}
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(tree);
cb.setParentIds(parents);
cb.setAuthor(original.getAuthorIdent());
cb.setMessage(commitMessage);
if (committerIdent != null) {
cb.setCommitter(committerIdent);
} else {
PersonIdent committerIdent =
Optional.ofNullable(original.getCommitterIdent())
.map(ident -> ctx.newCommitterIdent(ident.getEmailAddress(), ctx.getIdentifiedUser()))
.orElseGet(ctx::newCommitterIdent);
cb.setCommitter(committerIdent);
}
if (matchAuthorToCommitterDate) {
cb.setAuthor(
new PersonIdent(
cb.getAuthor(), cb.getCommitter().getWhen(), cb.getCommitter().getTimeZone()));
}
ObjectId objectId = ctx.getInserter().insert(cb);
CodeReviewCommit commit = ((CodeReviewRevWalk) ctx.getRevWalk()).parseCommit(objectId);
commit.setFilesWithGitConflicts(filesWithGitConflicts);
logger.atFine().log("rebased commit=%s", commit.name());
return commit;
}
}