blob: 9ea9dcbd949d7319dbbcbb4974314df142cc27c4 [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.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.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.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.git.strategy.CommitMergeStatus;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectState;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 Logger log = LoggerFactory.getLogger(MergeUtil.class);
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.isUseContentMerge());
}
@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);
}
Collections.sort(result, 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' + 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, user, 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, CurrentUser user, PatchSet.Id psId) {
try {
return approvalsUtil.byPatchSet(db.get(), notes, user, psId, null, null);
} catch (OrmException e) {
log.error("Can't read approval records for " + psId, e);
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) {
log.warn("Cannot merge due to LargeObjectException: " + 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.isMatchAuthorToCommitterDate()) {
commit.setAuthor(
new PersonIdent(
commit.getAuthor(),
commit.getCommitter().getWhen(),
commit.getCommitter().getTimeZone()));
}
}
}