All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.change.RebaseChangeOp Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy