884 lines
30 KiB
Java
884 lines
30 KiB
Java
// 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.git;
|
|
|
|
import static com.google.common.base.Preconditions.checkArgument;
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.base.Strings;
|
|
import com.google.common.collect.FluentIterable;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.Sets;
|
|
import com.google.common.flogger.FluentLogger;
|
|
import com.google.gerrit.common.FooterConstants;
|
|
import com.google.gerrit.common.Nullable;
|
|
import com.google.gerrit.common.data.LabelType;
|
|
import com.google.gerrit.extensions.registration.DynamicSet;
|
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
|
import com.google.gerrit.extensions.restapi.MergeConflictException;
|
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
|
import com.google.gerrit.reviewdb.client.Account;
|
|
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
|
|
import com.google.gerrit.reviewdb.client.Branch;
|
|
import com.google.gerrit.reviewdb.client.Change;
|
|
import com.google.gerrit.reviewdb.client.LabelId;
|
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
import com.google.gerrit.reviewdb.client.PatchSet.Id;
|
|
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
import com.google.gerrit.server.ApprovalsUtil;
|
|
import com.google.gerrit.server.CurrentUser;
|
|
import com.google.gerrit.server.IdentifiedUser;
|
|
import com.google.gerrit.server.config.CanonicalWebUrl;
|
|
import com.google.gerrit.server.config.GerritServerConfig;
|
|
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
|
|
import com.google.gerrit.server.notedb.ChangeNotes;
|
|
import com.google.gerrit.server.project.ProjectState;
|
|
import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
|
|
import com.google.gerrit.server.submit.CommitMergeStatus;
|
|
import com.google.gerrit.server.submit.IntegrationException;
|
|
import com.google.gerrit.server.submit.MergeIdenticalTreeException;
|
|
import com.google.gerrit.server.submit.MergeSorter;
|
|
import com.google.gwtorm.server.OrmException;
|
|
import com.google.inject.Inject;
|
|
import com.google.inject.Provider;
|
|
import com.google.inject.assistedinject.Assisted;
|
|
import com.google.inject.assistedinject.AssistedInject;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import org.eclipse.jgit.errors.AmbiguousObjectException;
|
|
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
|
import org.eclipse.jgit.errors.LargeObjectException;
|
|
import org.eclipse.jgit.errors.MissingObjectException;
|
|
import org.eclipse.jgit.errors.NoMergeBaseException;
|
|
import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
|
|
import org.eclipse.jgit.errors.RevisionSyntaxException;
|
|
import org.eclipse.jgit.lib.AnyObjectId;
|
|
import org.eclipse.jgit.lib.CommitBuilder;
|
|
import org.eclipse.jgit.lib.Config;
|
|
import org.eclipse.jgit.lib.Constants;
|
|
import org.eclipse.jgit.lib.ObjectId;
|
|
import org.eclipse.jgit.lib.ObjectInserter;
|
|
import org.eclipse.jgit.lib.PersonIdent;
|
|
import org.eclipse.jgit.lib.Repository;
|
|
import org.eclipse.jgit.merge.MergeStrategy;
|
|
import org.eclipse.jgit.merge.Merger;
|
|
import org.eclipse.jgit.merge.ResolveMerger;
|
|
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
|
|
import org.eclipse.jgit.merge.ThreeWayMerger;
|
|
import org.eclipse.jgit.revwalk.FooterKey;
|
|
import org.eclipse.jgit.revwalk.FooterLine;
|
|
import org.eclipse.jgit.revwalk.RevCommit;
|
|
import org.eclipse.jgit.revwalk.RevFlag;
|
|
import org.eclipse.jgit.revwalk.RevSort;
|
|
import org.eclipse.jgit.revwalk.RevWalk;
|
|
|
|
/**
|
|
* Utility methods used during the merge process.
|
|
*
|
|
* <p><strong>Note:</strong> Unless otherwise specified, the methods in this class <strong>do
|
|
* not</strong> flush {@link ObjectInserter}s. Callers that want to read back objects before
|
|
* flushing should use {@link ObjectInserter#newReader()}. This is already the default behavior of
|
|
* {@code BatchUpdate}.
|
|
*/
|
|
public class MergeUtil {
|
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
|
|
static class PluggableCommitMessageGenerator {
|
|
private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
|
|
|
|
@Inject
|
|
PluggableCommitMessageGenerator(DynamicSet<ChangeMessageModifier> changeMessageModifiers) {
|
|
this.changeMessageModifiers = changeMessageModifiers;
|
|
}
|
|
|
|
public String generate(
|
|
RevCommit original, RevCommit mergeTip, Branch.NameKey dest, String current) {
|
|
checkNotNull(original.getRawBuffer());
|
|
if (mergeTip != null) {
|
|
checkNotNull(mergeTip.getRawBuffer());
|
|
}
|
|
for (ChangeMessageModifier changeMessageModifier : changeMessageModifiers) {
|
|
current = changeMessageModifier.onSubmit(current, original, mergeTip, dest);
|
|
checkNotNull(
|
|
current,
|
|
changeMessageModifier.getClass().getName()
|
|
+ ".OnSubmit returned null instead of new commit message");
|
|
}
|
|
return current;
|
|
}
|
|
}
|
|
|
|
private static final String R_HEADS_MASTER = Constants.R_HEADS + Constants.MASTER;
|
|
|
|
public static boolean useRecursiveMerge(Config cfg) {
|
|
return cfg.getBoolean("core", null, "useRecursiveMerge", true);
|
|
}
|
|
|
|
public static ThreeWayMergeStrategy getMergeStrategy(Config cfg) {
|
|
return useRecursiveMerge(cfg) ? MergeStrategy.RECURSIVE : MergeStrategy.RESOLVE;
|
|
}
|
|
|
|
public interface Factory {
|
|
MergeUtil create(ProjectState project);
|
|
|
|
MergeUtil create(ProjectState project, boolean useContentMerge);
|
|
}
|
|
|
|
private final Provider<ReviewDb> db;
|
|
private final IdentifiedUser.GenericFactory identifiedUserFactory;
|
|
private final Provider<String> urlProvider;
|
|
private final ApprovalsUtil approvalsUtil;
|
|
private final ProjectState project;
|
|
private final boolean useContentMerge;
|
|
private final boolean useRecursiveMerge;
|
|
private final PluggableCommitMessageGenerator commitMessageGenerator;
|
|
|
|
@AssistedInject
|
|
MergeUtil(
|
|
@GerritServerConfig Config serverConfig,
|
|
Provider<ReviewDb> db,
|
|
IdentifiedUser.GenericFactory identifiedUserFactory,
|
|
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
|
|
ApprovalsUtil approvalsUtil,
|
|
PluggableCommitMessageGenerator commitMessageGenerator,
|
|
@Assisted ProjectState project) {
|
|
this(
|
|
serverConfig,
|
|
db,
|
|
identifiedUserFactory,
|
|
urlProvider,
|
|
approvalsUtil,
|
|
project,
|
|
commitMessageGenerator,
|
|
project.is(BooleanProjectConfig.USE_CONTENT_MERGE));
|
|
}
|
|
|
|
@AssistedInject
|
|
MergeUtil(
|
|
@GerritServerConfig Config serverConfig,
|
|
Provider<ReviewDb> db,
|
|
IdentifiedUser.GenericFactory identifiedUserFactory,
|
|
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
|
|
ApprovalsUtil approvalsUtil,
|
|
@Assisted ProjectState project,
|
|
PluggableCommitMessageGenerator commitMessageGenerator,
|
|
@Assisted boolean useContentMerge) {
|
|
this.db = db;
|
|
this.identifiedUserFactory = identifiedUserFactory;
|
|
this.urlProvider = urlProvider;
|
|
this.approvalsUtil = approvalsUtil;
|
|
this.project = project;
|
|
this.useContentMerge = useContentMerge;
|
|
this.useRecursiveMerge = useRecursiveMerge(serverConfig);
|
|
this.commitMessageGenerator = commitMessageGenerator;
|
|
}
|
|
|
|
public CodeReviewCommit getFirstFastForward(
|
|
CodeReviewCommit mergeTip, RevWalk rw, List<CodeReviewCommit> toMerge)
|
|
throws IntegrationException {
|
|
for (Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext(); ) {
|
|
try {
|
|
final CodeReviewCommit n = i.next();
|
|
if (mergeTip == null || rw.isMergedInto(mergeTip, n)) {
|
|
i.remove();
|
|
return n;
|
|
}
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Cannot fast-forward test during merge", e);
|
|
}
|
|
}
|
|
return mergeTip;
|
|
}
|
|
|
|
public List<CodeReviewCommit> reduceToMinimalMerge(
|
|
MergeSorter mergeSorter, Collection<CodeReviewCommit> toSort) throws IntegrationException {
|
|
List<CodeReviewCommit> result = new ArrayList<>();
|
|
try {
|
|
result.addAll(mergeSorter.sort(toSort));
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Branch head sorting failed", e);
|
|
}
|
|
result.sort(CodeReviewCommit.ORDER);
|
|
return result;
|
|
}
|
|
|
|
public CodeReviewCommit createCherryPickFromCommit(
|
|
ObjectInserter inserter,
|
|
Config repoConfig,
|
|
RevCommit mergeTip,
|
|
RevCommit originalCommit,
|
|
PersonIdent cherryPickCommitterIdent,
|
|
String commitMsg,
|
|
CodeReviewRevWalk rw,
|
|
int parentIndex,
|
|
boolean ignoreIdenticalTree)
|
|
throws MissingObjectException, IncorrectObjectTypeException, IOException,
|
|
MergeIdenticalTreeException, MergeConflictException {
|
|
|
|
final ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
|
|
|
|
m.setBase(originalCommit.getParent(parentIndex));
|
|
if (m.merge(mergeTip, originalCommit)) {
|
|
ObjectId tree = m.getResultTreeId();
|
|
if (tree.equals(mergeTip.getTree()) && !ignoreIdenticalTree) {
|
|
throw new MergeIdenticalTreeException("identical tree");
|
|
}
|
|
|
|
CommitBuilder mergeCommit = new CommitBuilder();
|
|
mergeCommit.setTreeId(tree);
|
|
mergeCommit.setParentId(mergeTip);
|
|
mergeCommit.setAuthor(originalCommit.getAuthorIdent());
|
|
mergeCommit.setCommitter(cherryPickCommitterIdent);
|
|
mergeCommit.setMessage(commitMsg);
|
|
matchAuthorToCommitterDate(project, mergeCommit);
|
|
return rw.parseCommit(inserter.insert(mergeCommit));
|
|
}
|
|
throw new MergeConflictException("merge conflict");
|
|
}
|
|
|
|
public static RevCommit createMergeCommit(
|
|
ObjectInserter inserter,
|
|
Config repoConfig,
|
|
RevCommit mergeTip,
|
|
RevCommit originalCommit,
|
|
String mergeStrategy,
|
|
PersonIdent committerIndent,
|
|
String commitMsg,
|
|
RevWalk rw)
|
|
throws IOException, MergeIdenticalTreeException, MergeConflictException {
|
|
|
|
if (!MergeStrategy.THEIRS.getName().equals(mergeStrategy)
|
|
&& rw.isMergedInto(originalCommit, mergeTip)) {
|
|
throw new ChangeAlreadyMergedException(
|
|
"'" + originalCommit.getName() + "' has already been merged");
|
|
}
|
|
|
|
Merger m = newMerger(inserter, repoConfig, mergeStrategy);
|
|
if (m.merge(false, mergeTip, originalCommit)) {
|
|
ObjectId tree = m.getResultTreeId();
|
|
|
|
CommitBuilder mergeCommit = new CommitBuilder();
|
|
mergeCommit.setTreeId(tree);
|
|
mergeCommit.setParentIds(mergeTip, originalCommit);
|
|
mergeCommit.setAuthor(committerIndent);
|
|
mergeCommit.setCommitter(committerIndent);
|
|
mergeCommit.setMessage(commitMsg);
|
|
return rw.parseCommit(inserter.insert(mergeCommit));
|
|
}
|
|
List<String> conflicts = ImmutableList.of();
|
|
if (m instanceof ResolveMerger) {
|
|
conflicts = ((ResolveMerger) m).getUnmergedPaths();
|
|
}
|
|
throw new MergeConflictException(createConflictMessage(conflicts));
|
|
}
|
|
|
|
public static String createConflictMessage(List<String> conflicts) {
|
|
StringBuilder sb = new StringBuilder("merge conflict(s)");
|
|
for (String c : conflicts) {
|
|
sb.append('\n').append(c);
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Adds footers to existing commit message based on the state of the change.
|
|
*
|
|
* <p>This adds the following footers if they are missing:
|
|
*
|
|
* <ul>
|
|
* <li>Reviewed-on: <i>url</i>
|
|
* <li>Reviewed-by | Tested-by | <i>Other-Label-Name</i>: <i>reviewer</i>
|
|
* <li>Change-Id
|
|
* </ul>
|
|
*
|
|
* @param n
|
|
* @param notes
|
|
* @param user
|
|
* @param psId
|
|
* @return new message
|
|
*/
|
|
private String createDetailedCommitMessage(
|
|
RevCommit n, ChangeNotes notes, CurrentUser user, PatchSet.Id psId) {
|
|
Change c = notes.getChange();
|
|
final List<FooterLine> footers = n.getFooterLines();
|
|
final StringBuilder msgbuf = new StringBuilder();
|
|
msgbuf.append(n.getFullMessage());
|
|
|
|
if (msgbuf.length() == 0) {
|
|
// WTF, an empty commit message?
|
|
msgbuf.append("<no commit message provided>");
|
|
}
|
|
if (msgbuf.charAt(msgbuf.length() - 1) != '\n') {
|
|
// Missing a trailing LF? Correct it (perhaps the editor was broken).
|
|
msgbuf.append('\n');
|
|
}
|
|
if (footers.isEmpty()) {
|
|
// Doesn't end in a "Signed-off-by: ..." style line? Add another line
|
|
// break to start a new paragraph for the reviewed-by tag lines.
|
|
//
|
|
msgbuf.append('\n');
|
|
}
|
|
|
|
if (!contains(footers, FooterConstants.CHANGE_ID, c.getKey().get())) {
|
|
msgbuf.append(FooterConstants.CHANGE_ID.getName());
|
|
msgbuf.append(": ");
|
|
msgbuf.append(c.getKey().get());
|
|
msgbuf.append('\n');
|
|
}
|
|
|
|
final String siteUrl = urlProvider.get();
|
|
if (siteUrl != null) {
|
|
final String url = siteUrl + c.getId().get();
|
|
if (!contains(footers, FooterConstants.REVIEWED_ON, url)) {
|
|
msgbuf.append(FooterConstants.REVIEWED_ON.getName());
|
|
msgbuf.append(": ");
|
|
msgbuf.append(url);
|
|
msgbuf.append('\n');
|
|
}
|
|
}
|
|
|
|
PatchSetApproval submitAudit = null;
|
|
|
|
for (PatchSetApproval a : safeGetApprovals(notes, psId)) {
|
|
if (a.getValue() <= 0) {
|
|
// Negative votes aren't counted.
|
|
continue;
|
|
}
|
|
|
|
if (a.isLegacySubmit()) {
|
|
// Submit is treated specially, below (becomes committer)
|
|
//
|
|
if (submitAudit == null || a.getGranted().compareTo(submitAudit.getGranted()) > 0) {
|
|
submitAudit = a;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
final Account acc = identifiedUserFactory.create(a.getAccountId()).getAccount();
|
|
final StringBuilder identbuf = new StringBuilder();
|
|
if (acc.getFullName() != null && acc.getFullName().length() > 0) {
|
|
if (identbuf.length() > 0) {
|
|
identbuf.append(' ');
|
|
}
|
|
identbuf.append(acc.getFullName());
|
|
}
|
|
if (acc.getPreferredEmail() != null && acc.getPreferredEmail().length() > 0) {
|
|
if (isSignedOffBy(footers, acc.getPreferredEmail())) {
|
|
continue;
|
|
}
|
|
if (identbuf.length() > 0) {
|
|
identbuf.append(' ');
|
|
}
|
|
identbuf.append('<');
|
|
identbuf.append(acc.getPreferredEmail());
|
|
identbuf.append('>');
|
|
}
|
|
if (identbuf.length() == 0) {
|
|
// Nothing reasonable to describe them by? Ignore them.
|
|
continue;
|
|
}
|
|
|
|
final String tag;
|
|
if (isCodeReview(a.getLabelId())) {
|
|
tag = "Reviewed-by";
|
|
} else if (isVerified(a.getLabelId())) {
|
|
tag = "Tested-by";
|
|
} else {
|
|
final LabelType lt = project.getLabelTypes().byLabel(a.getLabelId());
|
|
if (lt == null) {
|
|
continue;
|
|
}
|
|
tag = lt.getName();
|
|
}
|
|
|
|
if (!contains(footers, new FooterKey(tag), identbuf.toString())) {
|
|
msgbuf.append(tag);
|
|
msgbuf.append(": ");
|
|
msgbuf.append(identbuf);
|
|
msgbuf.append('\n');
|
|
}
|
|
}
|
|
return msgbuf.toString();
|
|
}
|
|
|
|
public String createCommitMessageOnSubmit(CodeReviewCommit n, RevCommit mergeTip) {
|
|
return createCommitMessageOnSubmit(
|
|
n,
|
|
mergeTip,
|
|
n.notes(),
|
|
identifiedUserFactory.create(n.notes().getChange().getOwner()),
|
|
n.getPatchsetId());
|
|
}
|
|
|
|
/**
|
|
* Creates a commit message for a change, which can be customized by plugins.
|
|
*
|
|
* <p>By default, adds footers to existing commit message based on the state of the change.
|
|
* Plugins implementing {@link ChangeMessageModifier} can modify the resulting commit message
|
|
* arbitrarily.
|
|
*
|
|
* @param n
|
|
* @param mergeTip
|
|
* @param notes
|
|
* @param user
|
|
* @param id
|
|
* @return new message
|
|
*/
|
|
public String createCommitMessageOnSubmit(
|
|
RevCommit n, RevCommit mergeTip, ChangeNotes notes, CurrentUser user, Id id) {
|
|
return commitMessageGenerator.generate(
|
|
n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, user, id));
|
|
}
|
|
|
|
private static boolean isCodeReview(LabelId id) {
|
|
return "Code-Review".equalsIgnoreCase(id.get());
|
|
}
|
|
|
|
private static boolean isVerified(LabelId id) {
|
|
return "Verified".equalsIgnoreCase(id.get());
|
|
}
|
|
|
|
private Iterable<PatchSetApproval> safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) {
|
|
try {
|
|
return approvalsUtil.byPatchSet(db.get(), notes, psId, null, null);
|
|
} catch (OrmException e) {
|
|
logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
|
|
return Collections.emptyList();
|
|
}
|
|
}
|
|
|
|
private static boolean contains(List<FooterLine> footers, FooterKey key, String val) {
|
|
for (FooterLine line : footers) {
|
|
if (line.matches(key) && val.equals(line.getValue())) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static boolean isSignedOffBy(List<FooterLine> footers, String email) {
|
|
for (FooterLine line : footers) {
|
|
if (line.matches(FooterKey.SIGNED_OFF_BY) && email.equals(line.getEmailAddress())) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean canMerge(
|
|
MergeSorter mergeSorter, Repository repo, CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
|
|
throws IntegrationException {
|
|
if (hasMissingDependencies(mergeSorter, toMerge)) {
|
|
return false;
|
|
}
|
|
|
|
try (ObjectInserter ins = new InMemoryInserter(repo)) {
|
|
return newThreeWayMerger(ins, repo.getConfig()).merge(new AnyObjectId[] {mergeTip, toMerge});
|
|
} catch (LargeObjectException e) {
|
|
logger.atWarning().log("Cannot merge due to LargeObjectException: %s", toMerge.name());
|
|
return false;
|
|
} catch (NoMergeBaseException e) {
|
|
return false;
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Cannot merge " + toMerge.name(), e);
|
|
}
|
|
}
|
|
|
|
public boolean canFastForward(
|
|
MergeSorter mergeSorter,
|
|
CodeReviewCommit mergeTip,
|
|
CodeReviewRevWalk rw,
|
|
CodeReviewCommit toMerge)
|
|
throws IntegrationException {
|
|
if (hasMissingDependencies(mergeSorter, toMerge)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return mergeTip == null
|
|
|| rw.isMergedInto(mergeTip, toMerge)
|
|
|| rw.isMergedInto(toMerge, mergeTip);
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Cannot fast-forward test during merge", e);
|
|
}
|
|
}
|
|
|
|
public boolean canCherryPick(
|
|
MergeSorter mergeSorter,
|
|
Repository repo,
|
|
CodeReviewCommit mergeTip,
|
|
CodeReviewRevWalk rw,
|
|
CodeReviewCommit toMerge)
|
|
throws IntegrationException {
|
|
if (mergeTip == null) {
|
|
// The branch is unborn. Fast-forward is possible.
|
|
//
|
|
return true;
|
|
}
|
|
|
|
if (toMerge.getParentCount() == 0) {
|
|
// Refuse to merge a root commit into an existing branch,
|
|
// we cannot obtain a delta for the cherry-pick to apply.
|
|
//
|
|
return false;
|
|
}
|
|
|
|
if (toMerge.getParentCount() == 1) {
|
|
// If there is only one parent, a cherry-pick can be done by
|
|
// taking the delta relative to that one parent and redoing
|
|
// that on the current merge tip.
|
|
//
|
|
try (ObjectInserter ins = new InMemoryInserter(repo)) {
|
|
ThreeWayMerger m = newThreeWayMerger(ins, repo.getConfig());
|
|
m.setBase(toMerge.getParent(0));
|
|
return m.merge(mergeTip, toMerge);
|
|
} catch (IOException e) {
|
|
throw new IntegrationException(
|
|
String.format(
|
|
"Cannot merge commit %s with mergetip %s", toMerge.name(), mergeTip.name()),
|
|
e);
|
|
}
|
|
}
|
|
|
|
// There are multiple parents, so this is a merge commit. We
|
|
// don't want to cherry-pick the merge as clients can't easily
|
|
// rebase their history with that merge present and replaced
|
|
// by an equivalent merge with a different first parent. So
|
|
// instead behave as though MERGE_IF_NECESSARY was configured.
|
|
//
|
|
return canFastForward(mergeSorter, mergeTip, rw, toMerge)
|
|
|| canMerge(mergeSorter, repo, mergeTip, toMerge);
|
|
}
|
|
|
|
public boolean hasMissingDependencies(MergeSorter mergeSorter, CodeReviewCommit toMerge)
|
|
throws IntegrationException {
|
|
try {
|
|
return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Branch head sorting failed", e);
|
|
}
|
|
}
|
|
|
|
public CodeReviewCommit mergeOneCommit(
|
|
PersonIdent author,
|
|
PersonIdent committer,
|
|
CodeReviewRevWalk rw,
|
|
ObjectInserter inserter,
|
|
Config repoConfig,
|
|
Branch.NameKey destBranch,
|
|
CodeReviewCommit mergeTip,
|
|
CodeReviewCommit n)
|
|
throws IntegrationException {
|
|
ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
|
|
try {
|
|
if (m.merge(new AnyObjectId[] {mergeTip, n})) {
|
|
return writeMergeCommit(
|
|
author, committer, rw, inserter, destBranch, mergeTip, m.getResultTreeId(), n);
|
|
}
|
|
failed(rw, mergeTip, n, CommitMergeStatus.PATH_CONFLICT);
|
|
} catch (NoMergeBaseException e) {
|
|
try {
|
|
failed(rw, mergeTip, n, getCommitMergeStatus(e.getReason()));
|
|
} catch (IOException e2) {
|
|
throw new IntegrationException("Cannot merge " + n.name(), e);
|
|
}
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Cannot merge " + n.name(), e);
|
|
}
|
|
return mergeTip;
|
|
}
|
|
|
|
private static CommitMergeStatus getCommitMergeStatus(MergeBaseFailureReason reason) {
|
|
switch (reason) {
|
|
case MULTIPLE_MERGE_BASES_NOT_SUPPORTED:
|
|
case TOO_MANY_MERGE_BASES:
|
|
default:
|
|
return CommitMergeStatus.MANUAL_RECURSIVE_MERGE;
|
|
case CONFLICTS_DURING_MERGE_BASE_CALCULATION:
|
|
return CommitMergeStatus.PATH_CONFLICT;
|
|
}
|
|
}
|
|
|
|
private static CodeReviewCommit failed(
|
|
CodeReviewRevWalk rw,
|
|
CodeReviewCommit mergeTip,
|
|
CodeReviewCommit n,
|
|
CommitMergeStatus failure)
|
|
throws MissingObjectException, IncorrectObjectTypeException, IOException {
|
|
rw.reset();
|
|
rw.markStart(n);
|
|
rw.markUninteresting(mergeTip);
|
|
CodeReviewCommit failed;
|
|
while ((failed = rw.next()) != null) {
|
|
failed.setStatusCode(failure);
|
|
}
|
|
return failed;
|
|
}
|
|
|
|
public CodeReviewCommit writeMergeCommit(
|
|
PersonIdent author,
|
|
PersonIdent committer,
|
|
CodeReviewRevWalk rw,
|
|
ObjectInserter inserter,
|
|
Branch.NameKey destBranch,
|
|
CodeReviewCommit mergeTip,
|
|
ObjectId treeId,
|
|
CodeReviewCommit n)
|
|
throws IOException, MissingObjectException, IncorrectObjectTypeException {
|
|
final List<CodeReviewCommit> merged = new ArrayList<>();
|
|
rw.reset();
|
|
rw.markStart(n);
|
|
rw.markUninteresting(mergeTip);
|
|
CodeReviewCommit crc;
|
|
while ((crc = rw.next()) != null) {
|
|
if (crc.getPatchsetId() != null) {
|
|
merged.add(crc);
|
|
}
|
|
}
|
|
|
|
StringBuilder msgbuf = new StringBuilder().append(summarize(rw, merged));
|
|
if (!R_HEADS_MASTER.equals(destBranch.get())) {
|
|
msgbuf.append(" into ");
|
|
msgbuf.append(destBranch.getShortName());
|
|
}
|
|
|
|
if (merged.size() > 1) {
|
|
msgbuf.append("\n\n* changes:\n");
|
|
for (CodeReviewCommit c : merged) {
|
|
rw.parseBody(c);
|
|
msgbuf.append(" ");
|
|
msgbuf.append(c.getShortMessage());
|
|
msgbuf.append("\n");
|
|
}
|
|
}
|
|
|
|
final CommitBuilder mergeCommit = new CommitBuilder();
|
|
mergeCommit.setTreeId(treeId);
|
|
mergeCommit.setParentIds(mergeTip, n);
|
|
mergeCommit.setAuthor(author);
|
|
mergeCommit.setCommitter(committer);
|
|
mergeCommit.setMessage(msgbuf.toString());
|
|
|
|
CodeReviewCommit mergeResult = rw.parseCommit(inserter.insert(mergeCommit));
|
|
mergeResult.setNotes(n.getNotes());
|
|
return mergeResult;
|
|
}
|
|
|
|
private String summarize(RevWalk rw, List<CodeReviewCommit> merged) throws IOException {
|
|
if (merged.size() == 1) {
|
|
CodeReviewCommit c = merged.get(0);
|
|
rw.parseBody(c);
|
|
return String.format("Merge \"%s\"", c.getShortMessage());
|
|
}
|
|
|
|
LinkedHashSet<String> topics = new LinkedHashSet<>(4);
|
|
for (CodeReviewCommit c : merged) {
|
|
if (!Strings.isNullOrEmpty(c.change().getTopic())) {
|
|
topics.add(c.change().getTopic());
|
|
}
|
|
}
|
|
|
|
if (topics.size() == 1) {
|
|
return String.format("Merge changes from topic \"%s\"", Iterables.getFirst(topics, null));
|
|
} else if (topics.size() > 1) {
|
|
return String.format("Merge changes from topics \"%s\"", Joiner.on("\", \"").join(topics));
|
|
} else {
|
|
return String.format(
|
|
"Merge changes %s%s",
|
|
FluentIterable.from(merged)
|
|
.limit(5)
|
|
.transform(c -> c.change().getKey().abbreviate())
|
|
.join(Joiner.on(',')),
|
|
merged.size() > 5 ? ", ..." : "");
|
|
}
|
|
}
|
|
|
|
public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) {
|
|
return newThreeWayMerger(inserter, repoConfig, mergeStrategyName());
|
|
}
|
|
|
|
public String mergeStrategyName() {
|
|
return mergeStrategyName(useContentMerge, useRecursiveMerge);
|
|
}
|
|
|
|
public static String mergeStrategyName(boolean useContentMerge, boolean useRecursiveMerge) {
|
|
if (useContentMerge) {
|
|
// Settings for this project allow us to try and automatically resolve
|
|
// conflicts within files if needed. Use either the old resolve merger or
|
|
// new recursive merger, and instruct to operate in core.
|
|
if (useRecursiveMerge) {
|
|
return MergeStrategy.RECURSIVE.getName();
|
|
}
|
|
return MergeStrategy.RESOLVE.getName();
|
|
}
|
|
// No auto conflict resolving allowed. If any of the
|
|
// affected files was modified, merge will fail.
|
|
return MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.getName();
|
|
}
|
|
|
|
public static ThreeWayMerger newThreeWayMerger(
|
|
ObjectInserter inserter, Config repoConfig, String strategyName) {
|
|
Merger m = newMerger(inserter, repoConfig, strategyName);
|
|
checkArgument(
|
|
m instanceof ThreeWayMerger,
|
|
"merge strategy %s does not support three-way merging",
|
|
strategyName);
|
|
return (ThreeWayMerger) m;
|
|
}
|
|
|
|
public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName) {
|
|
MergeStrategy strategy = MergeStrategy.get(strategyName);
|
|
checkArgument(strategy != null, "invalid merge strategy: %s", strategyName);
|
|
return strategy.newMerger(
|
|
new ObjectInserter.Filter() {
|
|
@Override
|
|
protected ObjectInserter delegate() {
|
|
return inserter;
|
|
}
|
|
|
|
@Override
|
|
public void flush() {}
|
|
|
|
@Override
|
|
public void close() {}
|
|
},
|
|
repoConfig);
|
|
}
|
|
|
|
public void markCleanMerges(
|
|
RevWalk rw, RevFlag canMergeFlag, CodeReviewCommit mergeTip, Set<RevCommit> alreadyAccepted)
|
|
throws IntegrationException {
|
|
if (mergeTip == null) {
|
|
// If mergeTip is null here, branchTip was null, indicating a new branch
|
|
// at the start of the merge process. We also elected to merge nothing,
|
|
// probably due to missing dependencies. Nothing was cleanly merged.
|
|
//
|
|
return;
|
|
}
|
|
|
|
try {
|
|
rw.resetRetain(canMergeFlag);
|
|
rw.sort(RevSort.TOPO);
|
|
rw.sort(RevSort.REVERSE, true);
|
|
rw.markStart(mergeTip);
|
|
for (RevCommit c : alreadyAccepted) {
|
|
// If branch was not created by this submit.
|
|
if (!Objects.equals(c, mergeTip)) {
|
|
rw.markUninteresting(c);
|
|
}
|
|
}
|
|
|
|
CodeReviewCommit c;
|
|
while ((c = (CodeReviewCommit) rw.next()) != null) {
|
|
if (c.getPatchsetId() != null && c.getStatusCode() == null) {
|
|
c.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Cannot mark clean merges", e);
|
|
}
|
|
}
|
|
|
|
public Set<Change.Id> findUnmergedChanges(
|
|
Set<Change.Id> expected,
|
|
CodeReviewRevWalk rw,
|
|
RevFlag canMergeFlag,
|
|
CodeReviewCommit oldTip,
|
|
CodeReviewCommit mergeTip,
|
|
Iterable<Change.Id> alreadyMerged)
|
|
throws IntegrationException {
|
|
if (mergeTip == null) {
|
|
return expected;
|
|
}
|
|
|
|
try {
|
|
Set<Change.Id> found = Sets.newHashSetWithExpectedSize(expected.size());
|
|
Iterables.addAll(found, alreadyMerged);
|
|
rw.resetRetain(canMergeFlag);
|
|
rw.sort(RevSort.TOPO);
|
|
rw.markStart(mergeTip);
|
|
if (oldTip != null) {
|
|
rw.markUninteresting(oldTip);
|
|
}
|
|
|
|
CodeReviewCommit c;
|
|
while ((c = rw.next()) != null) {
|
|
if (c.getPatchsetId() == null) {
|
|
continue;
|
|
}
|
|
Change.Id id = c.getPatchsetId().getParentKey();
|
|
if (!expected.contains(id)) {
|
|
continue;
|
|
}
|
|
found.add(id);
|
|
if (found.size() == expected.size()) {
|
|
return Collections.emptySet();
|
|
}
|
|
}
|
|
return Sets.difference(expected, found);
|
|
} catch (IOException e) {
|
|
throw new IntegrationException("Cannot check if changes were merged", e);
|
|
}
|
|
}
|
|
|
|
public static CodeReviewCommit findAnyMergedInto(
|
|
CodeReviewRevWalk rw, Iterable<CodeReviewCommit> commits, CodeReviewCommit tip)
|
|
throws IOException {
|
|
for (CodeReviewCommit c : commits) {
|
|
// TODO(dborowitz): Seems like this could get expensive for many patch
|
|
// sets. Is there a more efficient implementation?
|
|
if (rw.isMergedInto(c, tip)) {
|
|
return c;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str)
|
|
throws BadRequestException, ResourceNotFoundException, IOException {
|
|
try {
|
|
ObjectId commitId = repo.resolve(str);
|
|
if (commitId == null) {
|
|
throw new BadRequestException("Cannot resolve '" + str + "' to a commit");
|
|
}
|
|
return rw.parseCommit(commitId);
|
|
} catch (AmbiguousObjectException | IncorrectObjectTypeException | RevisionSyntaxException e) {
|
|
throw new BadRequestException(e.getMessage());
|
|
} catch (MissingObjectException e) {
|
|
throw new ResourceNotFoundException(e.getMessage());
|
|
}
|
|
}
|
|
|
|
private static void matchAuthorToCommitterDate(ProjectState project, CommitBuilder commit) {
|
|
if (project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE)) {
|
|
commit.setAuthor(
|
|
new PersonIdent(
|
|
commit.getAuthor(),
|
|
commit.getCommitter().getWhen(),
|
|
commit.getCommitter().getTimeZone()));
|
|
}
|
|
}
|
|
}
|