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

com.google.gerrit.server.git.MergeUtil Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// 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.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.joining;

import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
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.entities.Account;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.exceptions.InvalidMergeStrategyException;
import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.UrlFormatter;
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.MergeIdenticalTreeException;
import com.google.gerrit.server.submit.MergeSorter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
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.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.MergeFormatter;
import org.eclipse.jgit.merge.MergeResult;
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.eclipse.jgit.util.TemporaryBuffer;

/**
 * Utility methods used during the merge process.
 *
 * 

Note: Unless otherwise specified, the methods in this class do * not 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}. */ @AutoFactory public class MergeUtil { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); /** * Length of abbreviated hex SHA-1s in merged filenames. * *

This is a constant so output is stable over time even if the SHA-1 prefix becomes ambiguous. */ private static final int NAME_ABBREV_LEN = 6; 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; } private final IdentifiedUser.GenericFactory identifiedUserFactory; private final DynamicItem urlFormatter; private final ApprovalsUtil approvalsUtil; private final ProjectState project; private final boolean useContentMerge; private final boolean useRecursiveMerge; private final PluggableCommitMessageGenerator commitMessageGenerator; private final ChangeUtil changeUtil; MergeUtil( @Provided @GerritServerConfig Config serverConfig, @Provided IdentifiedUser.GenericFactory identifiedUserFactory, @Provided DynamicItem urlFormatter, @Provided ApprovalsUtil approvalsUtil, @Provided PluggableCommitMessageGenerator commitMessageGenerator, @Provided ChangeUtil changeUtil, ProjectState project) { this( serverConfig, identifiedUserFactory, urlFormatter, approvalsUtil, commitMessageGenerator, changeUtil, project, project.is(BooleanProjectConfig.USE_CONTENT_MERGE)); } MergeUtil( @Provided @GerritServerConfig Config serverConfig, @Provided IdentifiedUser.GenericFactory identifiedUserFactory, @Provided DynamicItem urlFormatter, @Provided ApprovalsUtil approvalsUtil, @Provided PluggableCommitMessageGenerator commitMessageGenerator, @Provided ChangeUtil changeUtil, ProjectState project, boolean useContentMerge) { this.identifiedUserFactory = identifiedUserFactory; this.urlFormatter = urlFormatter; this.approvalsUtil = approvalsUtil; this.commitMessageGenerator = commitMessageGenerator; this.changeUtil = changeUtil; this.project = project; this.useContentMerge = useContentMerge; this.useRecursiveMerge = useRecursiveMerge(serverConfig); } public CodeReviewCommit getFirstFastForward( CodeReviewCommit mergeTip, RevWalk rw, List toMerge) { for (Iterator 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 StorageException("Cannot fast-forward test during merge", e); } } return mergeTip; } public List reduceToMinimalMerge( MergeSorter mergeSorter, Collection toSort) { List result = new ArrayList<>(); try { result.addAll(mergeSorter.sort(toSort)); } catch (IOException | StorageException e) { throw new StorageException("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, boolean allowConflicts) throws IOException, MergeIdenticalTreeException, MergeConflictException, MethodNotAllowedException, InvalidMergeStrategyException { ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig); m.setBase(originalCommit.getParent(parentIndex)); DirCache dc = DirCache.newInCore(); if (allowConflicts && m instanceof ResolveMerger) { // The DirCache must be set on ResolveMerger before calling // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated. ((ResolveMerger) m).setDirCache(dc); } ObjectId tree; ImmutableSet filesWithGitConflicts; if (m.merge(mergeTip, originalCommit)) { filesWithGitConflicts = null; tree = m.getResultTreeId(); logger.atFine().log( "CherryPick treeId=%s (no conflicts, inserter: %s)", tree.name(), m.getObjectInserter()); if (tree.equals(mergeTip.getTree()) && !ignoreIdenticalTree) { throw new MergeIdenticalTreeException("identical tree"); } } else { if (!allowConflicts) { throw new MergeConflictException( String.format( "merge conflict while merging commits %s and %s", mergeTip.toObjectId(), originalCommit.toObjectId())); } if (!useContentMerge) { // If content merge is disabled we don't have a ResolveMerger and hence cannot merge with // conflict markers. throw new MethodNotAllowedException( "Cherry-pick with allow conflicts requires that content merge is enabled."); } // For merging with conflict markers we need a ResolveMerger, double-check that we have one. checkState(m instanceof ResolveMerger, "allow conflicts is not supported"); if (m.getResultTreeId() != null) { // Merging with conflicts below uses the same DirCache instance that has been used by the // Merger to attempt the merge without conflicts. // // The Merger uses the DirCache to do the updates, and in particular to write the result // tree. DirCache caches a single DirCacheTree instance that is used to write the result // tree, but it writes the result tree only if there were no conflicts. // // Merging with conflicts uses the same DirCache instance to write the tree with conflicts // that has been used by the Merger. This means if the Merger unexpectedly wrote a result // tree although there had been conflicts, then merging with conflicts uses the same // DirCacheTree instance to write the tree with conflicts. However DirCacheTree#writeTree // writes a tree only once and then that tree is cached. Further invocations of // DirCacheTree#writeTree have no effect and return the previously created tree. This means // merging with conflicts can only successfully create the tree with conflicts if the Merger // didn't write a result tree yet. Hence this is checked here and we log a warning if the // result tree was already written. logger.atWarning().log( "result tree has already been written: %s (merge: %s, conflicts: %s, failed: %s)", m, m.getResultTreeId().name(), ((ResolveMerger) m).getUnmergedPaths(), ((ResolveMerger) m).getFailingPaths()); } Map> mergeResults = ((ResolveMerger) m).getMergeResults(); filesWithGitConflicts = mergeResults.entrySet().stream() .filter(e -> e.getValue().containsConflicts()) .map(Map.Entry::getKey) .collect(toImmutableSet()); tree = mergeWithConflicts( rw, inserter, dc, "HEAD", mergeTip, "CHANGE", originalCommit, mergeResults); logger.atFine().log( "AutoMerge treeId=%s (with conflicts, inserter: %s)", tree.name(), inserter); } CommitBuilder cherryPickCommit = new CommitBuilder(); cherryPickCommit.setTreeId(tree); cherryPickCommit.setParentId(mergeTip); cherryPickCommit.setAuthor(originalCommit.getAuthorIdent()); cherryPickCommit.setCommitter(cherryPickCommitterIdent); cherryPickCommit.setMessage(commitMsg); matchAuthorToCommitterDate(project, cherryPickCommit); CodeReviewCommit commit = rw.parseCommit(inserter.insert(cherryPickCommit)); commit.setFilesWithGitConflicts(filesWithGitConflicts); logger.atFine().log("CherryPick commitId=%s", commit.name()); return commit; } public static ObjectId mergeWithConflicts( RevWalk rw, ObjectInserter ins, DirCache dc, String oursName, RevCommit ours, String theirsName, RevCommit theirs, Map> mergeResults) throws IOException { return mergeWithConflicts(rw, ins, dc, oursName, ours, theirsName, theirs, mergeResults, false); } @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading. public static ObjectId mergeWithConflicts( RevWalk rw, ObjectInserter ins, DirCache dc, String oursName, RevCommit ours, String theirsName, RevCommit theirs, Map> mergeResults, boolean diff3Format) throws IOException { rw.parseBody(ours); rw.parseBody(theirs); String oursMsg = ours.getShortMessage(); String theirsMsg = theirs.getShortMessage(); int nameLength = Math.max(oursName.length(), theirsName.length()); String oursNameFormatted = String.format( "%-" + nameLength + "s (%s %s)", oursName, abbreviateName(ours, NAME_ABBREV_LEN), oursMsg.substring(0, Math.min(oursMsg.length(), 60))); String theirsNameFormatted = String.format( "%-" + nameLength + "s (%s %s)", theirsName, abbreviateName(theirs, NAME_ABBREV_LEN), theirsMsg.substring(0, Math.min(theirsMsg.length(), 60))); MergeFormatter fmt = new MergeFormatter(); Map resolved = new HashMap<>(); for (Map.Entry> entry : mergeResults.entrySet()) { MergeResult p = entry.getValue(); TemporaryBuffer buf = null; try { // TODO(dborowitz): Respect inCoreLimit here. buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024); if (diff3Format) { fmt.formatMergeDiff3(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8); } else { fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8); } buf.close(); // Flush file and close for writes, but leave available for reading. try (InputStream in = buf.openInputStream()) { resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in)); } } finally { if (buf != null) { buf.destroy(); } } } DirCacheBuilder builder = dc.builder(); int cnt = dc.getEntryCount(); for (int i = 0; i < cnt; ) { DirCacheEntry entry = dc.getEntry(i); if (entry.getStage() == 0) { builder.add(entry); i++; continue; } int next = dc.nextEntry(i); String path = entry.getPathString(); DirCacheEntry res = new DirCacheEntry(path); if (resolved.containsKey(path)) { // For a file with content merge conflict that we produced a result // above on, collapse the file down to a single stage 0 with just // the blob content, and a randomly selected mode (the lowest stage, // which should be the merge base, or ours). res.setFileMode(entry.getFileMode()); res.setObjectId(resolved.get(path)); } else if (next == i + 1) { // If there is exactly one stage present, shouldn't be a conflict... res.setFileMode(entry.getFileMode()); res.setObjectId(entry.getObjectId()); } else if (next == i + 2) { // Two stages suggests a delete/modify conflict. Pick the higher // stage as the automatic result. entry = dc.getEntry(i + 1); res.setFileMode(entry.getFileMode()); res.setObjectId(entry.getObjectId()); } else { // 3 stage conflict, no resolve above // Punt on the 3-stage conflict and show the base, for now. res.setFileMode(entry.getFileMode()); res.setObjectId(entry.getObjectId()); } builder.add(res); i = next; } builder.finish(); return dc.writeTree(ins); } public static CodeReviewCommit createMergeCommit( ObjectInserter inserter, Config repoConfig, RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy, boolean allowConflicts, PersonIdent committerIdent, String commitMsg, CodeReviewRevWalk rw) throws IOException, MergeIdenticalTreeException, MergeConflictException, InvalidMergeStrategyException { return createMergeCommit( inserter, repoConfig, mergeTip, originalCommit, mergeStrategy, allowConflicts, committerIdent, committerIdent, commitMsg, rw); } public static CodeReviewCommit createMergeCommit( ObjectInserter inserter, Config repoConfig, RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy, boolean allowConflicts, PersonIdent authorIdent, PersonIdent committerIdent, String commitMsg, CodeReviewRevWalk rw) throws IOException, MergeIdenticalTreeException, MergeConflictException, InvalidMergeStrategyException { 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); DirCache dc = DirCache.newInCore(); if (allowConflicts && m instanceof ResolveMerger) { // The DirCache must be set on ResolveMerger before calling // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated. ((ResolveMerger) m).setDirCache(dc); } ObjectId tree; ImmutableSet filesWithGitConflicts; if (m.merge(false, mergeTip, originalCommit)) { filesWithGitConflicts = null; tree = m.getResultTreeId(); } else { List conflicts = ImmutableList.of(); Map failed = ImmutableMap.of(); if (m instanceof ResolveMerger) { conflicts = ((ResolveMerger) m).getUnmergedPaths(); failed = ((ResolveMerger) m).getFailingPaths(); } if (m.getResultTreeId() != null) { // Merging with conflicts below uses the same DirCache instance that has been used by the // Merger to attempt the merge without conflicts. // // The Merger uses the DirCache to do the updates, and in particular to write the result // tree. DirCache caches a single DirCacheTree instance that is used to write the result // tree, but it writes the result tree only if there were no conflicts. // // Merging with conflicts uses the same DirCache instance to write the tree with conflicts // that has been used by the Merger. This means if the Merger unexpectedly wrote a result // tree although there had been conflicts, then merging with conflicts uses the same // DirCacheTree instance to write the tree with conflicts. However DirCacheTree#writeTree // writes a tree only once and then that tree is cached. Further invocations of // DirCacheTree#writeTree have no effect and return the previously created tree. This means // merging with conflicts can only successfully create the tree with conflicts if the Merger // didn't write a result tree yet. Hence this is checked here and we log a warning if the // result tree was already written. logger.atWarning().log( "result tree has already been written: %s (merge: %s, conflicts: %s, failed: %s)", m, m.getResultTreeId().name(), conflicts, failed); } if (!allowConflicts) { throw new MergeConflictException(createConflictMessage(conflicts)); } // For merging with conflict markers we need a ResolveMerger, double-check that we have one. if (!(m instanceof ResolveMerger)) { throw new MergeWithConflictsNotSupportedException(MergeStrategy.get(mergeStrategy)); } Map> mergeResults = ((ResolveMerger) m).getMergeResults(); filesWithGitConflicts = mergeResults.entrySet().stream() .filter(e -> e.getValue().containsConflicts()) .map(Map.Entry::getKey) .collect(toImmutableSet()); tree = mergeWithConflicts( rw, inserter, dc, "TARGET BRANCH", mergeTip, "SOURCE BRANCH", originalCommit, mergeResults); } CommitBuilder mergeCommit = new CommitBuilder(); mergeCommit.setTreeId(tree); mergeCommit.setParentIds(mergeTip, originalCommit); mergeCommit.setAuthor(authorIdent); mergeCommit.setCommitter(committerIdent); mergeCommit.setMessage(commitMsg); CodeReviewCommit commit = rw.parseCommit(inserter.insert(mergeCommit)); commit.setFilesWithGitConflicts(filesWithGitConflicts); return commit; } public static String createConflictMessage(List conflicts) { if (conflicts.isEmpty()) { return ""; } 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. * *

This adds the following footers if they are missing: * *

    *
  • Reviewed-on: url *
  • Reviewed-by | Tested-by | Other-Label-Name: reviewer *
  • Change-Id *
* * @return new message */ private String createDetailedCommitMessage(RevCommit n, ChangeNotes notes, PatchSet.Id psId) { Change c = notes.getChange(); final List footers = n.getFooterLines(); final StringBuilder msgbuf = new StringBuilder(); msgbuf.append(n.getFullMessage()); if (msgbuf.length() == 0) { // WTF, an empty commit message? msgbuf.append(""); } 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 (changeUtil.getChangeIdsFromFooter(n).isEmpty()) { msgbuf.append(FooterConstants.CHANGE_ID.getName()); msgbuf.append(": "); msgbuf.append(c.getKey().get()); msgbuf.append('\n'); } Optional url = urlFormatter.get().getChangeViewUrl(c.getProject(), c.getId()); if (url.isPresent()) { if (!contains(footers, FooterConstants.REVIEWED_ON, url.get())) { msgbuf .append(FooterConstants.REVIEWED_ON.getName()) .append(": ") .append(url.get()) .append('\n'); } } PatchSetApproval submitAudit = null; for (PatchSetApproval a : safeGetApprovals(notes, psId)) { if (a.value() <= 0) { // Negative votes aren't counted. continue; } if (a.isLegacySubmit()) { // Submit is treated specially, below (becomes committer) // if (submitAudit == null || a.granted().compareTo(submitAudit.granted()) > 0) { submitAudit = a; } continue; } final Account acc = identifiedUserFactory.create(a.accountId()).getAccount(); final StringBuilder identbuf = new StringBuilder(); if (acc.fullName() != null && acc.fullName().length() > 0) { if (identbuf.length() > 0) { identbuf.append(' '); } identbuf.append(acc.fullName()); } if (acc.preferredEmail() != null && acc.preferredEmail().length() > 0) { if (isSignedOffBy(footers, acc.preferredEmail())) { continue; } if (identbuf.length() > 0) { identbuf.append(' '); } identbuf.append('<'); identbuf.append(acc.preferredEmail()); identbuf.append('>'); } if (identbuf.length() == 0) { // Nothing reasonable to describe them by? Ignore them. continue; } final String tag; if (isCodeReview(a.labelId())) { tag = "Reviewed-by"; } else if (isVerified(a.labelId())) { tag = "Tested-by"; } else { final Optional lt = project.getLabelTypes().byLabel(a.labelId()); if (!lt.isPresent()) { continue; } tag = lt.get().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(), n.getPatchsetId()); } /** * Creates a commit message for a change, which can be customized by plugins. * *

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. * * @return new message */ public String createCommitMessageOnSubmit( RevCommit n, @Nullable RevCommit mergeTip, ChangeNotes notes, PatchSet.Id id) { return commitMessageGenerator.generate( n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, id)); } private static boolean isCodeReview(LabelId id) { return LabelId.CODE_REVIEW.equalsIgnoreCase(id.get()); } private static boolean isVerified(LabelId id) { return LabelId.VERIFIED.equalsIgnoreCase(id.get()); } private Iterable safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) { try { return approvalsUtil.byPatchSet(notes, psId); } catch (StorageException e) { logger.atSevere().withCause(e).log("Can't read approval records for %s", psId); return Collections.emptyList(); } } private static boolean contains(List 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 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) { if (hasMissingDependencies(mergeSorter, toMerge)) { return false; } return canMerge(mergeTip, repo, toMerge); } private boolean canMerge(CodeReviewCommit mergeTip, Repository repo, CodeReviewCommit toMerge) { try (ObjectInserter ins = new InMemoryInserter(repo)) { return newThreeWayMerger(ins, repo.getConfig()).merge(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 StorageException("Cannot merge " + toMerge.name(), e); } } public boolean canFastForward( MergeSorter mergeSorter, CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge) { if (hasMissingDependencies(mergeSorter, toMerge)) { return false; } return canFastForward(mergeTip, rw, toMerge); } private boolean canFastForward( CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge) { try { return mergeTip == null || rw.isMergedInto(mergeTip, toMerge) || rw.isMergedInto(toMerge, mergeTip); } catch (IOException e) { throw new StorageException("Cannot fast-forward test during merge", e); } } public boolean canFastForwardOrMerge( MergeSorter mergeSorter, CodeReviewCommit mergeTip, CodeReviewRevWalk rw, Repository repo, CodeReviewCommit toMerge) { if (hasMissingDependencies(mergeSorter, toMerge)) { return false; } return canFastForward(mergeTip, rw, toMerge) || canMerge(mergeTip, repo, toMerge); } public boolean canCherryPick( MergeSorter mergeSorter, Repository repo, CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge) { 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 StorageException( 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 canFastForwardOrMerge(mergeSorter, mergeTip, rw, repo, toMerge); } public boolean hasMissingDependencies(MergeSorter mergeSorter, CodeReviewCommit toMerge) { try { return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge); } catch (IOException | StorageException e) { throw new StorageException("Branch head sorting failed", e); } } public CodeReviewCommit mergeOneCommit( PersonIdent author, PersonIdent committer, CodeReviewRevWalk rw, ObjectInserter inserter, Config repoConfig, BranchNameKey destBranch, CodeReviewCommit mergeTip, CodeReviewCommit n) throws InvalidMergeStrategyException { ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig); try { if (m.merge(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 StorageException("Cannot merge " + n.name(), e2); } } catch (IOException e) { throw new StorageException("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; } } /** * Marks all commits that are reachable from the given commit {@code n} as failed by setting the * provided {@code failure} status code on them. * *

If the same commits are retrieved from the same {@link CodeReviewRevWalk} instance later the * status code that we set here can be read there. */ private static void 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); } } public CodeReviewCommit writeMergeCommit( PersonIdent author, PersonIdent committer, CodeReviewRevWalk rw, ObjectInserter inserter, BranchNameKey destBranch, CodeReviewCommit mergeTip, ObjectId treeId, CodeReviewCommit n) throws IOException, MissingObjectException, IncorrectObjectTypeException { final List 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.branch())) { msgbuf.append(" into "); msgbuf.append(destBranch.shortName()); } 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 merged) throws IOException { if (merged.size() == 1) { CodeReviewCommit c = merged.get(0); rw.parseBody(c); return String.format("Merge \"%s\"", c.getShortMessage()); } ImmutableSortedSet topics = merged.stream() .map(c -> c.change().getTopic()) .filter(t -> !Strings.isNullOrEmpty(t)) .map(t -> "\"" + t + "\"") .collect(toImmutableSortedSet(naturalOrder())); if (!topics.isEmpty()) { return String.format( "Merge changes from topic%s %s", topics.size() > 1 ? "s" : "", topics.stream().collect(joining(", "))); } return merged.stream() .limit(5) .map(c -> c.change().getKey().abbreviate()) .collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : "")); } public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) throws InvalidMergeStrategyException { return newThreeWayMerger(inserter, repoConfig, mergeStrategyName()); } public String mergeStrategyName() { return mergeStrategyName(useContentMerge, useRecursiveMerge); } public static String mergeStrategyName(boolean useContentMerge, boolean useRecursiveMerge) { String mergeStrategy; 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) { mergeStrategy = MergeStrategy.RECURSIVE.getName(); } else { mergeStrategy = MergeStrategy.RESOLVE.getName(); } } else { // No auto conflict resolving allowed. If any of the // affected files was modified, merge will fail. mergeStrategy = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.getName(); } logger.atFine().log( "mergeStrategy = %s (useContentMerge = %s, useRecursiveMerge = %s)", mergeStrategy, useContentMerge, useRecursiveMerge); return mergeStrategy; } public static ThreeWayMerger newThreeWayMerger( ObjectInserter inserter, Config repoConfig, String strategyName) throws InvalidMergeStrategyException { 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) throws InvalidMergeStrategyException { MergeStrategy strategy = MergeStrategy.get(strategyName); if (strategy == null) { throw new InvalidMergeStrategyException(strategyName); } return strategy.newMerger( new ObjectInserter.Filter() { @Override protected ObjectInserter delegate() { return inserter; } @Override public void flush() {} @Override public void close() {} @Override public String toString() { return String.format( "%s (wrapped inserter: %s)", super.toString(), inserter.toString()); } }, repoConfig); } public void markCleanMerges( RevWalk rw, RevFlag canMergeFlag, CodeReviewCommit mergeTip, Set alreadyAccepted) { 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 StorageException("Cannot mark clean merges", e); } } public Set findUnmergedChanges( Set expected, CodeReviewRevWalk rw, RevFlag canMergeFlag, CodeReviewCommit oldTip, CodeReviewCommit mergeTip, Iterable alreadyMerged) { if (mergeTip == null) { return expected; } try { Set 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().changeId(); 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 StorageException("Cannot check if changes were merged", e); } } @Nullable public static CodeReviewCommit findAnyMergedInto( CodeReviewRevWalk rw, Iterable 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())); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy