| // 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 com.google.common.base.Function; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Strings; |
| 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.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.PatchSetApproval; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.ApprovalsUtil; |
| 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.project.ChangeControl; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Provider; |
| import com.google.inject.assistedinject.Assisted; |
| import com.google.inject.assistedinject.AssistedInject; |
| |
| 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; |
| |
| 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.Set; |
| |
| /** |
| * 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); |
| 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; |
| |
| @AssistedInject |
| MergeUtil(@GerritServerConfig Config serverConfig, |
| final Provider<ReviewDb> db, |
| final IdentifiedUser.GenericFactory identifiedUserFactory, |
| @CanonicalWebUrl @Nullable final Provider<String> urlProvider, |
| final ApprovalsUtil approvalsUtil, |
| @Assisted final ProjectState project) { |
| this(serverConfig, db, identifiedUserFactory, urlProvider, approvalsUtil, |
| project, project.isUseContentMerge()); |
| } |
| |
| @AssistedInject |
| MergeUtil(@GerritServerConfig Config serverConfig, |
| final Provider<ReviewDb> db, |
| final IdentifiedUser.GenericFactory identifiedUserFactory, |
| @CanonicalWebUrl @Nullable final Provider<String> urlProvider, |
| final ApprovalsUtil approvalsUtil, |
| @Assisted final ProjectState project, |
| @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); |
| } |
| |
| public CodeReviewCommit getFirstFastForward( |
| final CodeReviewCommit mergeTip, final RevWalk rw, |
| final List<CodeReviewCommit> toMerge) throws IntegrationException { |
| for (final 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(Repository repo, |
| ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit, |
| PersonIdent cherryPickCommitterIdent, String commitMsg, |
| CodeReviewRevWalk rw) |
| throws MissingObjectException, IncorrectObjectTypeException, IOException, |
| MergeIdenticalTreeException, MergeConflictException { |
| |
| final ThreeWayMerger m = newThreeWayMerger(repo, inserter); |
| |
| m.setBase(originalCommit.getParent(0)); |
| if (m.merge(mergeTip, originalCommit)) { |
| ObjectId tree = m.getResultTreeId(); |
| if (tree.equals(mergeTip.getTree())) { |
| 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); |
| return rw.parseCommit(inserter.insert(mergeCommit)); |
| } |
| throw new MergeConflictException("merge conflict"); |
| } |
| |
| public static RevCommit createMergeCommit(Repository repo, ObjectInserter inserter, |
| RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy, |
| PersonIdent committerIndent, String commitMsg, RevWalk rw) |
| throws IOException, MergeIdenticalTreeException, MergeConflictException { |
| |
| if (rw.isMergedInto(originalCommit, mergeTip)) { |
| throw new ChangeAlreadyMergedException( |
| "'" + originalCommit.getName() + "' has already been merged"); |
| } |
| |
| Merger m = newMerger(repo, inserter, 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(); |
| } |
| |
| public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl, |
| PatchSet.Id psId) { |
| Change c = ctl.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 (final PatchSetApproval a : safeGetApprovals(ctl, 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 createCherryPickCommitMessage(final CodeReviewCommit n) { |
| return createCherryPickCommitMessage(n, n.getControl(), n.getPatchsetId()); |
| } |
| |
| 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( |
| ChangeControl ctl, PatchSet.Id psId) { |
| try { |
| return approvalsUtil.byPatchSet(db.get(), ctl, psId); |
| } 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 (final 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 (final FooterLine line : footers) { |
| if (line.matches(FooterKey.SIGNED_OFF_BY) |
| && email.equals(line.getEmailAddress())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean canMerge(final MergeSorter mergeSorter, |
| final Repository repo, final CodeReviewCommit mergeTip, |
| final CodeReviewCommit toMerge) |
| throws IntegrationException { |
| if (hasMissingDependencies(mergeSorter, toMerge)) { |
| return false; |
| } |
| |
| try (ObjectInserter ins = new InMemoryInserter(repo)) { |
| return newThreeWayMerger(repo, ins) |
| .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); |
| } 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(repo, ins); |
| 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(final MergeSorter mergeSorter, |
| final 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, Repository repo, CodeReviewRevWalk rw, |
| ObjectInserter inserter, Branch.NameKey destBranch, |
| CodeReviewCommit mergeTip, CodeReviewCommit n) |
| throws IntegrationException { |
| final ThreeWayMerger m = newThreeWayMerger(repo, inserter); |
| 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 (final 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.setControl(n.getControl()); |
| 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", |
| Joiner.on(',').join(Iterables.transform( |
| Iterables.limit(merged, 5), |
| new Function<CodeReviewCommit, String>() { |
| @Override |
| public String apply(CodeReviewCommit in) { |
| return in.change().getKey().abbreviate(); |
| } |
| })), |
| merged.size() > 5 ? ", ..." : ""); |
| } |
| } |
| |
| public ThreeWayMerger newThreeWayMerger(final Repository repo, |
| final ObjectInserter inserter) { |
| return newThreeWayMerger(repo, inserter, 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(Repository repo, |
| final ObjectInserter inserter, String strategyName) { |
| Merger m = newMerger(repo, inserter, strategyName); |
| checkArgument(m instanceof ThreeWayMerger, |
| "merge strategy %s does not support three-way merging", strategyName); |
| return (ThreeWayMerger) m; |
| } |
| |
| public static Merger newMerger(Repository repo, |
| final ObjectInserter inserter, String strategyName) { |
| MergeStrategy strategy = MergeStrategy.get(strategyName); |
| checkArgument(strategy != null, "invalid merge strategy: %s", strategyName); |
| Merger m = strategy.newMerger(repo, true); |
| m.setObjectInserter(new ObjectInserter.Filter() { |
| @Override |
| protected ObjectInserter delegate() { |
| return inserter; |
| } |
| |
| @Override |
| public void flush() { |
| } |
| |
| @Override |
| public void close() { |
| } |
| }); |
| return m; |
| } |
| |
| public void markCleanMerges(final RevWalk rw, |
| final RevFlag canMergeFlag, final CodeReviewCommit mergeTip, |
| final 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) { |
| 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()); |
| } |
| } |
| } |