| // 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.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.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, ChangeControl ctl, String current) { |
| checkNotNull(original.getRawBuffer()); |
| if (mergeTip != null) { |
| checkNotNull(mergeTip.getRawBuffer()); |
| } |
| for (ChangeMessageModifier changeMessageModifier : changeMessageModifiers) { |
| current = |
| changeMessageModifier.onSubmit(current, original, mergeTip, ctl.getChange().getDest()); |
| 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( |
| 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, Set<CodeReviewCommit> incoming) |
| throws IntegrationException { |
| List<CodeReviewCommit> result = new ArrayList<>(); |
| try { |
| result.addAll(mergeSorter.sort(toSort, incoming)); |
| } 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, |
| int parentIndex, |
| boolean ignoreIdenticalTree) |
| throws MissingObjectException, IncorrectObjectTypeException, IOException, |
| MergeIdenticalTreeException, MergeConflictException { |
| |
| final ThreeWayMerger m = newThreeWayMerger(repo, inserter); |
| |
| 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); |
| 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 (!MergeStrategy.THEIRS.getName().equals(mergeStrategy) |
| && 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(); |
| } |
| |
| /** |
| * 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 ctl |
| * @param psId |
| * @return new message |
| */ |
| private String createDetailedCommitMessage(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 createCommitMessageOnSubmit(CodeReviewCommit n, RevCommit mergeTip) { |
| return createCommitMessageOnSubmit(n, mergeTip, n.getControl(), 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 ctl |
| * @param id |
| * @return new message |
| */ |
| public String createCommitMessageOnSubmit( |
| RevCommit n, RevCommit mergeTip, ChangeControl ctl, Id id) { |
| return commitMessageGenerator.generate( |
| n, mergeTip, ctl, createDetailedCommitMessage(n, ctl, 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(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) |
| || 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(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", |
| FluentIterable.from(merged) |
| .limit(5) |
| .transform(c -> c.change().getKey().abbreviate()) |
| .join(Joiner.on(',')), |
| 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) { |
| // 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()); |
| } |
| } |
| } |