LCOV - code coverage report
Current view: top level - server/git - MergeUtil.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 366 428 85.5 %
Date: 2022-11-19 15:00:39 Functions: 48 49 98.0 %

          Line data    Source code
       1             : // Copyright (C) 2012 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.git;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : import static com.google.common.base.Preconditions.checkState;
      19             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      20             : import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
      21             : import static com.google.gerrit.git.ObjectIds.abbreviateName;
      22             : import static java.nio.charset.StandardCharsets.UTF_8;
      23             : import static java.util.Comparator.naturalOrder;
      24             : import static java.util.stream.Collectors.joining;
      25             : 
      26             : import com.google.auto.factory.AutoFactory;
      27             : import com.google.auto.factory.Provided;
      28             : import com.google.common.base.Strings;
      29             : import com.google.common.collect.ImmutableList;
      30             : import com.google.common.collect.ImmutableSet;
      31             : import com.google.common.collect.ImmutableSortedSet;
      32             : import com.google.common.collect.Iterables;
      33             : import com.google.common.collect.Sets;
      34             : import com.google.common.flogger.FluentLogger;
      35             : import com.google.gerrit.common.FooterConstants;
      36             : import com.google.gerrit.common.Nullable;
      37             : import com.google.gerrit.entities.Account;
      38             : import com.google.gerrit.entities.BooleanProjectConfig;
      39             : import com.google.gerrit.entities.BranchNameKey;
      40             : import com.google.gerrit.entities.Change;
      41             : import com.google.gerrit.entities.LabelId;
      42             : import com.google.gerrit.entities.LabelType;
      43             : import com.google.gerrit.entities.PatchSet;
      44             : import com.google.gerrit.entities.PatchSetApproval;
      45             : import com.google.gerrit.exceptions.InvalidMergeStrategyException;
      46             : import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
      47             : import com.google.gerrit.exceptions.StorageException;
      48             : import com.google.gerrit.extensions.registration.DynamicItem;
      49             : import com.google.gerrit.extensions.restapi.BadRequestException;
      50             : import com.google.gerrit.extensions.restapi.MergeConflictException;
      51             : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
      52             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      53             : import com.google.gerrit.server.ChangeUtil;
      54             : import com.google.gerrit.server.IdentifiedUser;
      55             : import com.google.gerrit.server.approval.ApprovalsUtil;
      56             : import com.google.gerrit.server.config.GerritServerConfig;
      57             : import com.google.gerrit.server.config.UrlFormatter;
      58             : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
      59             : import com.google.gerrit.server.notedb.ChangeNotes;
      60             : import com.google.gerrit.server.project.ProjectState;
      61             : import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
      62             : import com.google.gerrit.server.submit.CommitMergeStatus;
      63             : import com.google.gerrit.server.submit.MergeIdenticalTreeException;
      64             : import com.google.gerrit.server.submit.MergeSorter;
      65             : import java.io.IOException;
      66             : import java.io.InputStream;
      67             : import java.util.ArrayList;
      68             : import java.util.Collection;
      69             : import java.util.Collections;
      70             : import java.util.HashMap;
      71             : import java.util.Iterator;
      72             : import java.util.List;
      73             : import java.util.Map;
      74             : import java.util.Objects;
      75             : import java.util.Optional;
      76             : import java.util.Set;
      77             : import org.eclipse.jgit.diff.Sequence;
      78             : import org.eclipse.jgit.dircache.DirCache;
      79             : import org.eclipse.jgit.dircache.DirCacheBuilder;
      80             : import org.eclipse.jgit.dircache.DirCacheEntry;
      81             : import org.eclipse.jgit.errors.AmbiguousObjectException;
      82             : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
      83             : import org.eclipse.jgit.errors.LargeObjectException;
      84             : import org.eclipse.jgit.errors.MissingObjectException;
      85             : import org.eclipse.jgit.errors.NoMergeBaseException;
      86             : import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
      87             : import org.eclipse.jgit.errors.RevisionSyntaxException;
      88             : import org.eclipse.jgit.lib.CommitBuilder;
      89             : import org.eclipse.jgit.lib.Config;
      90             : import org.eclipse.jgit.lib.Constants;
      91             : import org.eclipse.jgit.lib.ObjectId;
      92             : import org.eclipse.jgit.lib.ObjectInserter;
      93             : import org.eclipse.jgit.lib.PersonIdent;
      94             : import org.eclipse.jgit.lib.Repository;
      95             : import org.eclipse.jgit.merge.MergeFormatter;
      96             : import org.eclipse.jgit.merge.MergeResult;
      97             : import org.eclipse.jgit.merge.MergeStrategy;
      98             : import org.eclipse.jgit.merge.Merger;
      99             : import org.eclipse.jgit.merge.ResolveMerger;
     100             : import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
     101             : import org.eclipse.jgit.merge.ThreeWayMerger;
     102             : import org.eclipse.jgit.revwalk.FooterKey;
     103             : import org.eclipse.jgit.revwalk.FooterLine;
     104             : import org.eclipse.jgit.revwalk.RevCommit;
     105             : import org.eclipse.jgit.revwalk.RevFlag;
     106             : import org.eclipse.jgit.revwalk.RevSort;
     107             : import org.eclipse.jgit.revwalk.RevWalk;
     108             : import org.eclipse.jgit.util.TemporaryBuffer;
     109             : 
     110             : /**
     111             :  * Utility methods used during the merge process.
     112             :  *
     113             :  * <p><strong>Note:</strong> Unless otherwise specified, the methods in this class <strong>do
     114             :  * not</strong> flush {@link ObjectInserter}s. Callers that want to read back objects before
     115             :  * flushing should use {@link ObjectInserter#newReader()}. This is already the default behavior of
     116             :  * {@code BatchUpdate}.
     117             :  */
     118             : @AutoFactory
     119             : public class MergeUtil {
     120         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     121             : 
     122             :   /**
     123             :    * Length of abbreviated hex SHA-1s in merged filenames.
     124             :    *
     125             :    * <p>This is a constant so output is stable over time even if the SHA-1 prefix becomes ambiguous.
     126             :    */
     127             :   private static final int NAME_ABBREV_LEN = 6;
     128             : 
     129             :   private static final String R_HEADS_MASTER = Constants.R_HEADS + Constants.MASTER;
     130             : 
     131             :   public static boolean useRecursiveMerge(Config cfg) {
     132         152 :     return cfg.getBoolean("core", null, "useRecursiveMerge", true);
     133             :   }
     134             : 
     135             :   public static ThreeWayMergeStrategy getMergeStrategy(Config cfg) {
     136         152 :     return useRecursiveMerge(cfg) ? MergeStrategy.RECURSIVE : MergeStrategy.RESOLVE;
     137             :   }
     138             : 
     139             :   private final IdentifiedUser.GenericFactory identifiedUserFactory;
     140             :   private final DynamicItem<UrlFormatter> urlFormatter;
     141             :   private final ApprovalsUtil approvalsUtil;
     142             :   private final ProjectState project;
     143             :   private final boolean useContentMerge;
     144             :   private final boolean useRecursiveMerge;
     145             :   private final PluggableCommitMessageGenerator commitMessageGenerator;
     146             : 
     147             :   MergeUtil(
     148             :       @Provided @GerritServerConfig Config serverConfig,
     149             :       @Provided IdentifiedUser.GenericFactory identifiedUserFactory,
     150             :       @Provided DynamicItem<UrlFormatter> urlFormatter,
     151             :       @Provided ApprovalsUtil approvalsUtil,
     152             :       @Provided PluggableCommitMessageGenerator commitMessageGenerator,
     153             :       ProjectState project) {
     154         103 :     this(
     155             :         serverConfig,
     156             :         identifiedUserFactory,
     157             :         urlFormatter,
     158             :         approvalsUtil,
     159             :         commitMessageGenerator,
     160             :         project,
     161         103 :         project.is(BooleanProjectConfig.USE_CONTENT_MERGE));
     162         103 :   }
     163             : 
     164             :   MergeUtil(
     165             :       @Provided @GerritServerConfig Config serverConfig,
     166             :       @Provided IdentifiedUser.GenericFactory identifiedUserFactory,
     167             :       @Provided DynamicItem<UrlFormatter> urlFormatter,
     168             :       @Provided ApprovalsUtil approvalsUtil,
     169             :       @Provided PluggableCommitMessageGenerator commitMessageGenerator,
     170             :       ProjectState project,
     171         103 :       boolean useContentMerge) {
     172         103 :     this.identifiedUserFactory = identifiedUserFactory;
     173         103 :     this.urlFormatter = urlFormatter;
     174         103 :     this.approvalsUtil = approvalsUtil;
     175         103 :     this.commitMessageGenerator = commitMessageGenerator;
     176         103 :     this.project = project;
     177         103 :     this.useContentMerge = useContentMerge;
     178         103 :     this.useRecursiveMerge = useRecursiveMerge(serverConfig);
     179         103 :   }
     180             : 
     181             :   public CodeReviewCommit getFirstFastForward(
     182             :       CodeReviewCommit mergeTip, RevWalk rw, List<CodeReviewCommit> toMerge) {
     183          53 :     for (Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext(); ) {
     184             :       try {
     185          53 :         final CodeReviewCommit n = i.next();
     186          53 :         if (mergeTip == null || rw.isMergedInto(mergeTip, n)) {
     187          53 :           i.remove();
     188          53 :           return n;
     189             :         }
     190           0 :       } catch (IOException e) {
     191           0 :         throw new StorageException("Cannot fast-forward test during merge", e);
     192          14 :       }
     193             :     }
     194          14 :     return mergeTip;
     195             :   }
     196             : 
     197             :   public List<CodeReviewCommit> reduceToMinimalMerge(
     198             :       MergeSorter mergeSorter, Collection<CodeReviewCommit> toSort) {
     199          53 :     List<CodeReviewCommit> result = new ArrayList<>();
     200             :     try {
     201          53 :       result.addAll(mergeSorter.sort(toSort));
     202           0 :     } catch (IOException | StorageException e) {
     203           0 :       throw new StorageException("Branch head sorting failed", e);
     204          53 :     }
     205          53 :     result.sort(CodeReviewCommit.ORDER);
     206          53 :     return result;
     207             :   }
     208             : 
     209             :   public CodeReviewCommit createCherryPickFromCommit(
     210             :       ObjectInserter inserter,
     211             :       Config repoConfig,
     212             :       RevCommit mergeTip,
     213             :       RevCommit originalCommit,
     214             :       PersonIdent cherryPickCommitterIdent,
     215             :       String commitMsg,
     216             :       CodeReviewRevWalk rw,
     217             :       int parentIndex,
     218             :       boolean ignoreIdenticalTree,
     219             :       boolean allowConflicts)
     220             :       throws IOException, MergeIdenticalTreeException, MergeConflictException,
     221             :           MethodNotAllowedException, InvalidMergeStrategyException {
     222             : 
     223          14 :     ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
     224          14 :     m.setBase(originalCommit.getParent(parentIndex));
     225             : 
     226          14 :     DirCache dc = DirCache.newInCore();
     227          14 :     if (allowConflicts && m instanceof ResolveMerger) {
     228             :       // The DirCache must be set on ResolveMerger before calling
     229             :       // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
     230           3 :       ((ResolveMerger) m).setDirCache(dc);
     231             :     }
     232             : 
     233             :     ObjectId tree;
     234             :     ImmutableSet<String> filesWithGitConflicts;
     235          14 :     if (m.merge(mergeTip, originalCommit)) {
     236          13 :       filesWithGitConflicts = null;
     237          13 :       tree = m.getResultTreeId();
     238          13 :       if (tree.equals(mergeTip.getTree()) && !ignoreIdenticalTree) {
     239           3 :         throw new MergeIdenticalTreeException("identical tree");
     240             :       }
     241             :     } else {
     242           3 :       if (!allowConflicts) {
     243           2 :         throw new MergeConflictException(
     244           2 :             String.format(
     245             :                 "merge conflict while merging commits %s and %s",
     246           2 :                 mergeTip.toObjectId(), originalCommit.toObjectId()));
     247             :       }
     248             : 
     249           2 :       if (!useContentMerge) {
     250             :         // If content merge is disabled we don't have a ResolveMerger and hence cannot merge with
     251             :         // conflict markers.
     252           0 :         throw new MethodNotAllowedException(
     253             :             "Cherry-pick with allow conflicts requires that content merge is enabled.");
     254             :       }
     255             : 
     256             :       // For merging with conflict markers we need a ResolveMerger, double-check that we have one.
     257           2 :       checkState(m instanceof ResolveMerger, "allow conflicts is not supported");
     258           2 :       Map<String, MergeResult<? extends Sequence>> mergeResults =
     259           2 :           ((ResolveMerger) m).getMergeResults();
     260             : 
     261           2 :       filesWithGitConflicts =
     262           2 :           mergeResults.entrySet().stream()
     263           2 :               .filter(e -> e.getValue().containsConflicts())
     264           2 :               .map(Map.Entry::getKey)
     265           2 :               .collect(toImmutableSet());
     266             : 
     267           2 :       tree =
     268           2 :           mergeWithConflicts(
     269             :               rw, inserter, dc, "HEAD", mergeTip, "CHANGE", originalCommit, mergeResults);
     270             :     }
     271             : 
     272          14 :     CommitBuilder cherryPickCommit = new CommitBuilder();
     273          14 :     cherryPickCommit.setTreeId(tree);
     274          14 :     cherryPickCommit.setParentId(mergeTip);
     275          14 :     cherryPickCommit.setAuthor(originalCommit.getAuthorIdent());
     276          14 :     cherryPickCommit.setCommitter(cherryPickCommitterIdent);
     277          14 :     cherryPickCommit.setMessage(commitMsg);
     278          14 :     matchAuthorToCommitterDate(project, cherryPickCommit);
     279          14 :     CodeReviewCommit commit = rw.parseCommit(inserter.insert(cherryPickCommit));
     280          14 :     commit.setFilesWithGitConflicts(filesWithGitConflicts);
     281          14 :     return commit;
     282             :   }
     283             : 
     284             :   @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
     285             :   public static ObjectId mergeWithConflicts(
     286             :       RevWalk rw,
     287             :       ObjectInserter ins,
     288             :       DirCache dc,
     289             :       String oursName,
     290             :       RevCommit ours,
     291             :       String theirsName,
     292             :       RevCommit theirs,
     293             :       Map<String, MergeResult<? extends Sequence>> mergeResults)
     294             :       throws IOException {
     295          26 :     rw.parseBody(ours);
     296          26 :     rw.parseBody(theirs);
     297          26 :     String oursMsg = ours.getShortMessage();
     298          26 :     String theirsMsg = theirs.getShortMessage();
     299             : 
     300          26 :     int nameLength = Math.max(oursName.length(), theirsName.length());
     301          26 :     String oursNameFormatted =
     302          26 :         String.format(
     303             :             "%-" + nameLength + "s (%s %s)",
     304             :             oursName,
     305          26 :             abbreviateName(ours, NAME_ABBREV_LEN),
     306          26 :             oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
     307          26 :     String theirsNameFormatted =
     308          26 :         String.format(
     309             :             "%-" + nameLength + "s (%s %s)",
     310             :             theirsName,
     311          26 :             abbreviateName(theirs, NAME_ABBREV_LEN),
     312          26 :             theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
     313             : 
     314          26 :     MergeFormatter fmt = new MergeFormatter();
     315          26 :     Map<String, ObjectId> resolved = new HashMap<>();
     316          26 :     for (Map.Entry<String, MergeResult<? extends Sequence>> entry : mergeResults.entrySet()) {
     317          26 :       MergeResult<? extends Sequence> p = entry.getValue();
     318          26 :       TemporaryBuffer buf = null;
     319             :       try {
     320             :         // TODO(dborowitz): Respect inCoreLimit here.
     321          26 :         buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
     322          26 :         fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
     323          26 :         buf.close(); // Flush file and close for writes, but leave available for reading.
     324             : 
     325          26 :         try (InputStream in = buf.openInputStream()) {
     326          26 :           resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
     327             :         }
     328             :       } finally {
     329          26 :         if (buf != null) {
     330          26 :           buf.destroy();
     331             :         }
     332             :       }
     333          26 :     }
     334             : 
     335          26 :     DirCacheBuilder builder = dc.builder();
     336          26 :     int cnt = dc.getEntryCount();
     337          26 :     for (int i = 0; i < cnt; ) {
     338          26 :       DirCacheEntry entry = dc.getEntry(i);
     339          26 :       if (entry.getStage() == 0) {
     340           7 :         builder.add(entry);
     341           7 :         i++;
     342           7 :         continue;
     343             :       }
     344             : 
     345          26 :       int next = dc.nextEntry(i);
     346          26 :       String path = entry.getPathString();
     347          26 :       DirCacheEntry res = new DirCacheEntry(path);
     348          26 :       if (resolved.containsKey(path)) {
     349             :         // For a file with content merge conflict that we produced a result
     350             :         // above on, collapse the file down to a single stage 0 with just
     351             :         // the blob content, and a randomly selected mode (the lowest stage,
     352             :         // which should be the merge base, or ours).
     353          26 :         res.setFileMode(entry.getFileMode());
     354          26 :         res.setObjectId(resolved.get(path));
     355             : 
     356           0 :       } else if (next == i + 1) {
     357             :         // If there is exactly one stage present, shouldn't be a conflict...
     358           0 :         res.setFileMode(entry.getFileMode());
     359           0 :         res.setObjectId(entry.getObjectId());
     360             : 
     361           0 :       } else if (next == i + 2) {
     362             :         // Two stages suggests a delete/modify conflict. Pick the higher
     363             :         // stage as the automatic result.
     364           0 :         entry = dc.getEntry(i + 1);
     365           0 :         res.setFileMode(entry.getFileMode());
     366           0 :         res.setObjectId(entry.getObjectId());
     367             : 
     368             :       } else {
     369             :         // 3 stage conflict, no resolve above
     370             :         // Punt on the 3-stage conflict and show the base, for now.
     371           0 :         res.setFileMode(entry.getFileMode());
     372           0 :         res.setObjectId(entry.getObjectId());
     373             :       }
     374          26 :       builder.add(res);
     375          26 :       i = next;
     376          26 :     }
     377          26 :     builder.finish();
     378          26 :     return dc.writeTree(ins);
     379             :   }
     380             : 
     381             :   public static CodeReviewCommit createMergeCommit(
     382             :       ObjectInserter inserter,
     383             :       Config repoConfig,
     384             :       RevCommit mergeTip,
     385             :       RevCommit originalCommit,
     386             :       String mergeStrategy,
     387             :       boolean allowConflicts,
     388             :       PersonIdent committerIdent,
     389             :       String commitMsg,
     390             :       CodeReviewRevWalk rw)
     391             :       throws IOException, MergeIdenticalTreeException, MergeConflictException,
     392             :           InvalidMergeStrategyException {
     393           1 :     return createMergeCommit(
     394             :         inserter,
     395             :         repoConfig,
     396             :         mergeTip,
     397             :         originalCommit,
     398             :         mergeStrategy,
     399             :         allowConflicts,
     400             :         committerIdent,
     401             :         committerIdent,
     402             :         commitMsg,
     403             :         rw);
     404             :   }
     405             : 
     406             :   public static CodeReviewCommit createMergeCommit(
     407             :       ObjectInserter inserter,
     408             :       Config repoConfig,
     409             :       RevCommit mergeTip,
     410             :       RevCommit originalCommit,
     411             :       String mergeStrategy,
     412             :       boolean allowConflicts,
     413             :       PersonIdent authorIdent,
     414             :       PersonIdent committerIdent,
     415             :       String commitMsg,
     416             :       CodeReviewRevWalk rw)
     417             :       throws IOException, MergeIdenticalTreeException, MergeConflictException,
     418             :           InvalidMergeStrategyException {
     419             : 
     420           3 :     if (!MergeStrategy.THEIRS.getName().equals(mergeStrategy)
     421           3 :         && rw.isMergedInto(originalCommit, mergeTip)) {
     422           1 :       throw new ChangeAlreadyMergedException(
     423           1 :           "'" + originalCommit.getName() + "' has already been merged");
     424             :     }
     425             : 
     426           3 :     Merger m = newMerger(inserter, repoConfig, mergeStrategy);
     427             : 
     428           3 :     DirCache dc = DirCache.newInCore();
     429           3 :     if (allowConflicts && m instanceof ResolveMerger) {
     430             :       // The DirCache must be set on ResolveMerger before calling
     431             :       // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
     432           2 :       ((ResolveMerger) m).setDirCache(dc);
     433             :     }
     434             : 
     435             :     ObjectId tree;
     436             :     ImmutableSet<String> filesWithGitConflicts;
     437           3 :     if (m.merge(false, mergeTip, originalCommit)) {
     438           3 :       filesWithGitConflicts = null;
     439           3 :       tree = m.getResultTreeId();
     440             :     } else {
     441           2 :       List<String> conflicts = ImmutableList.of();
     442           2 :       if (m instanceof ResolveMerger) {
     443           2 :         conflicts = ((ResolveMerger) m).getUnmergedPaths();
     444             :       }
     445             : 
     446           2 :       if (!allowConflicts) {
     447           2 :         throw new MergeConflictException(createConflictMessage(conflicts));
     448             :       }
     449             : 
     450             :       // For merging with conflict markers we need a ResolveMerger, double-check that we have one.
     451           2 :       if (!(m instanceof ResolveMerger)) {
     452           2 :         throw new MergeWithConflictsNotSupportedException(MergeStrategy.get(mergeStrategy));
     453             :       }
     454           2 :       Map<String, MergeResult<? extends Sequence>> mergeResults =
     455           2 :           ((ResolveMerger) m).getMergeResults();
     456             : 
     457           2 :       filesWithGitConflicts =
     458           2 :           mergeResults.entrySet().stream()
     459           2 :               .filter(e -> e.getValue().containsConflicts())
     460           2 :               .map(Map.Entry::getKey)
     461           2 :               .collect(toImmutableSet());
     462             : 
     463           2 :       tree =
     464           2 :           mergeWithConflicts(
     465             :               rw,
     466             :               inserter,
     467             :               dc,
     468             :               "TARGET BRANCH",
     469             :               mergeTip,
     470             :               "SOURCE BRANCH",
     471             :               originalCommit,
     472             :               mergeResults);
     473             :     }
     474             : 
     475           3 :     CommitBuilder mergeCommit = new CommitBuilder();
     476           3 :     mergeCommit.setTreeId(tree);
     477           3 :     mergeCommit.setParentIds(mergeTip, originalCommit);
     478           3 :     mergeCommit.setAuthor(authorIdent);
     479           3 :     mergeCommit.setCommitter(committerIdent);
     480           3 :     mergeCommit.setMessage(commitMsg);
     481           3 :     CodeReviewCommit commit = rw.parseCommit(inserter.insert(mergeCommit));
     482           3 :     commit.setFilesWithGitConflicts(filesWithGitConflicts);
     483           3 :     return commit;
     484             :   }
     485             : 
     486             :   public static String createConflictMessage(List<String> conflicts) {
     487           5 :     if (conflicts.isEmpty()) {
     488           0 :       return "";
     489             :     }
     490             : 
     491           5 :     StringBuilder sb = new StringBuilder("merge conflict(s):");
     492           5 :     for (String c : conflicts) {
     493           5 :       sb.append('\n').append(c);
     494           5 :     }
     495           5 :     return sb.toString();
     496             :   }
     497             : 
     498             :   /**
     499             :    * Adds footers to existing commit message based on the state of the change.
     500             :    *
     501             :    * <p>This adds the following footers if they are missing:
     502             :    *
     503             :    * <ul>
     504             :    *   <li>Reviewed-on: <i>url</i>
     505             :    *   <li>Reviewed-by | Tested-by | <i>Other-Label-Name</i>: <i>reviewer</i>
     506             :    *   <li>Change-Id
     507             :    * </ul>
     508             :    *
     509             :    * @return new message
     510             :    */
     511             :   private String createDetailedCommitMessage(RevCommit n, ChangeNotes notes, PatchSet.Id psId) {
     512         103 :     Change c = notes.getChange();
     513         103 :     final List<FooterLine> footers = n.getFooterLines();
     514         103 :     final StringBuilder msgbuf = new StringBuilder();
     515         103 :     msgbuf.append(n.getFullMessage());
     516             : 
     517         103 :     if (msgbuf.length() == 0) {
     518             :       // WTF, an empty commit message?
     519           4 :       msgbuf.append("<no commit message provided>");
     520             :     }
     521         103 :     if (msgbuf.charAt(msgbuf.length() - 1) != '\n') {
     522             :       // Missing a trailing LF? Correct it (perhaps the editor was broken).
     523          10 :       msgbuf.append('\n');
     524             :     }
     525         103 :     if (footers.isEmpty()) {
     526             :       // Doesn't end in a "Signed-off-by: ..." style line? Add another line
     527             :       // break to start a new paragraph for the reviewed-by tag lines.
     528             :       //
     529          10 :       msgbuf.append('\n');
     530             :     }
     531             : 
     532         103 :     if (ChangeUtil.getChangeIdsFromFooter(n, urlFormatter.get()).isEmpty()) {
     533          10 :       msgbuf.append(FooterConstants.CHANGE_ID.getName());
     534          10 :       msgbuf.append(": ");
     535          10 :       msgbuf.append(c.getKey().get());
     536          10 :       msgbuf.append('\n');
     537             :     }
     538             : 
     539         103 :     Optional<String> url = urlFormatter.get().getChangeViewUrl(c.getProject(), c.getId());
     540         103 :     if (url.isPresent()) {
     541         103 :       if (!contains(footers, FooterConstants.REVIEWED_ON, url.get())) {
     542         103 :         msgbuf
     543         103 :             .append(FooterConstants.REVIEWED_ON.getName())
     544         103 :             .append(": ")
     545         103 :             .append(url.get())
     546         103 :             .append('\n');
     547             :       }
     548             :     }
     549         103 :     PatchSetApproval submitAudit = null;
     550             : 
     551         103 :     for (PatchSetApproval a : safeGetApprovals(notes, psId)) {
     552          67 :       if (a.value() <= 0) {
     553             :         // Negative votes aren't counted.
     554          23 :         continue;
     555             :       }
     556             : 
     557          67 :       if (a.isLegacySubmit()) {
     558             :         // Submit is treated specially, below (becomes committer)
     559             :         //
     560          55 :         if (submitAudit == null || a.granted().compareTo(submitAudit.granted()) > 0) {
     561          55 :           submitAudit = a;
     562             :         }
     563             :         continue;
     564             :       }
     565             : 
     566          58 :       final Account acc = identifiedUserFactory.create(a.accountId()).getAccount();
     567          58 :       final StringBuilder identbuf = new StringBuilder();
     568          58 :       if (acc.fullName() != null && acc.fullName().length() > 0) {
     569          54 :         if (identbuf.length() > 0) {
     570           0 :           identbuf.append(' ');
     571             :         }
     572          54 :         identbuf.append(acc.fullName());
     573             :       }
     574          58 :       if (acc.preferredEmail() != null && acc.preferredEmail().length() > 0) {
     575          58 :         if (isSignedOffBy(footers, acc.preferredEmail())) {
     576           0 :           continue;
     577             :         }
     578          58 :         if (identbuf.length() > 0) {
     579          54 :           identbuf.append(' ');
     580             :         }
     581          58 :         identbuf.append('<');
     582          58 :         identbuf.append(acc.preferredEmail());
     583          58 :         identbuf.append('>');
     584             :       }
     585          58 :       if (identbuf.length() == 0) {
     586             :         // Nothing reasonable to describe them by? Ignore them.
     587           6 :         continue;
     588             :       }
     589             : 
     590             :       final String tag;
     591          58 :       if (isCodeReview(a.labelId())) {
     592          56 :         tag = "Reviewed-by";
     593          17 :       } else if (isVerified(a.labelId())) {
     594          11 :         tag = "Tested-by";
     595             :       } else {
     596           9 :         final Optional<LabelType> lt = project.getLabelTypes().byLabel(a.labelId());
     597           9 :         if (!lt.isPresent()) {
     598           0 :           continue;
     599             :         }
     600           9 :         tag = lt.get().getName();
     601             :       }
     602             : 
     603          58 :       if (!contains(footers, new FooterKey(tag), identbuf.toString())) {
     604          58 :         msgbuf.append(tag);
     605          58 :         msgbuf.append(": ");
     606          58 :         msgbuf.append(identbuf);
     607          58 :         msgbuf.append('\n');
     608             :       }
     609          58 :     }
     610         103 :     return msgbuf.toString();
     611             :   }
     612             : 
     613             :   public String createCommitMessageOnSubmit(CodeReviewCommit n, RevCommit mergeTip) {
     614           8 :     return createCommitMessageOnSubmit(n, mergeTip, n.notes(), n.getPatchsetId());
     615             :   }
     616             : 
     617             :   /**
     618             :    * Creates a commit message for a change, which can be customized by plugins.
     619             :    *
     620             :    * <p>By default, adds footers to existing commit message based on the state of the change.
     621             :    * Plugins implementing {@link ChangeMessageModifier} can modify the resulting commit message
     622             :    * arbitrarily.
     623             :    *
     624             :    * @return new message
     625             :    */
     626             :   public String createCommitMessageOnSubmit(
     627             :       RevCommit n, @Nullable RevCommit mergeTip, ChangeNotes notes, PatchSet.Id id) {
     628         103 :     return commitMessageGenerator.generate(
     629         103 :         n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, id));
     630             :   }
     631             : 
     632             :   private static boolean isCodeReview(LabelId id) {
     633          58 :     return LabelId.CODE_REVIEW.equalsIgnoreCase(id.get());
     634             :   }
     635             : 
     636             :   private static boolean isVerified(LabelId id) {
     637          17 :     return LabelId.VERIFIED.equalsIgnoreCase(id.get());
     638             :   }
     639             : 
     640             :   private Iterable<PatchSetApproval> safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) {
     641             :     try {
     642         103 :       return approvalsUtil.byPatchSet(notes, psId);
     643           0 :     } catch (StorageException e) {
     644           0 :       logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
     645           0 :       return Collections.emptyList();
     646             :     }
     647             :   }
     648             : 
     649             :   private static boolean contains(List<FooterLine> footers, FooterKey key, String val) {
     650         103 :     for (FooterLine line : footers) {
     651         103 :       if (line.matches(key) && val.equals(line.getValue())) {
     652           8 :         return true;
     653             :       }
     654         103 :     }
     655         103 :     return false;
     656             :   }
     657             : 
     658             :   private static boolean isSignedOffBy(List<FooterLine> footers, String email) {
     659          58 :     for (FooterLine line : footers) {
     660          58 :       if (line.matches(FooterKey.SIGNED_OFF_BY) && email.equals(line.getEmailAddress())) {
     661           0 :         return true;
     662             :       }
     663          58 :     }
     664          58 :     return false;
     665             :   }
     666             : 
     667             :   public boolean canMerge(
     668             :       MergeSorter mergeSorter,
     669             :       Repository repo,
     670             :       CodeReviewCommit mergeTip,
     671             :       CodeReviewCommit toMerge) {
     672          13 :     if (hasMissingDependencies(mergeSorter, toMerge)) {
     673           0 :       return false;
     674             :     }
     675             : 
     676          13 :     try (ObjectInserter ins = new InMemoryInserter(repo)) {
     677          13 :       return newThreeWayMerger(ins, repo.getConfig()).merge(mergeTip, toMerge);
     678           0 :     } catch (LargeObjectException e) {
     679           0 :       logger.atWarning().log("Cannot merge due to LargeObjectException: %s", toMerge.name());
     680           0 :       return false;
     681           0 :     } catch (NoMergeBaseException e) {
     682           0 :       return false;
     683           0 :     } catch (IOException e) {
     684           0 :       throw new StorageException("Cannot merge " + toMerge.name(), e);
     685             :     }
     686             :   }
     687             : 
     688             :   public boolean canFastForward(
     689             :       MergeSorter mergeSorter,
     690             :       CodeReviewCommit mergeTip,
     691             :       CodeReviewRevWalk rw,
     692             :       CodeReviewCommit toMerge) {
     693          23 :     if (hasMissingDependencies(mergeSorter, toMerge)) {
     694           2 :       return false;
     695             :     }
     696             : 
     697             :     try {
     698          23 :       return mergeTip == null
     699          23 :           || rw.isMergedInto(mergeTip, toMerge)
     700          23 :           || rw.isMergedInto(toMerge, mergeTip);
     701           0 :     } catch (IOException e) {
     702           0 :       throw new StorageException("Cannot fast-forward test during merge", e);
     703             :     }
     704             :   }
     705             : 
     706             :   public boolean canCherryPick(
     707             :       MergeSorter mergeSorter,
     708             :       Repository repo,
     709             :       CodeReviewCommit mergeTip,
     710             :       CodeReviewRevWalk rw,
     711             :       CodeReviewCommit toMerge) {
     712           3 :     if (mergeTip == null) {
     713             :       // The branch is unborn. Fast-forward is possible.
     714             :       //
     715           0 :       return true;
     716             :     }
     717             : 
     718           3 :     if (toMerge.getParentCount() == 0) {
     719             :       // Refuse to merge a root commit into an existing branch,
     720             :       // we cannot obtain a delta for the cherry-pick to apply.
     721             :       //
     722           0 :       return false;
     723             :     }
     724             : 
     725           3 :     if (toMerge.getParentCount() == 1) {
     726             :       // If there is only one parent, a cherry-pick can be done by
     727             :       // taking the delta relative to that one parent and redoing
     728             :       // that on the current merge tip.
     729             :       //
     730           3 :       try (ObjectInserter ins = new InMemoryInserter(repo)) {
     731           3 :         ThreeWayMerger m = newThreeWayMerger(ins, repo.getConfig());
     732           3 :         m.setBase(toMerge.getParent(0));
     733           3 :         return m.merge(mergeTip, toMerge);
     734           0 :       } catch (IOException e) {
     735           0 :         throw new StorageException(
     736           0 :             String.format(
     737           0 :                 "Cannot merge commit %s with mergetip %s", toMerge.name(), mergeTip.name()),
     738             :             e);
     739             :       }
     740             :     }
     741             : 
     742             :     // There are multiple parents, so this is a merge commit. We
     743             :     // don't want to cherry-pick the merge as clients can't easily
     744             :     // rebase their history with that merge present and replaced
     745             :     // by an equivalent merge with a different first parent. So
     746             :     // instead behave as though MERGE_IF_NECESSARY was configured.
     747             :     //
     748           1 :     return canFastForward(mergeSorter, mergeTip, rw, toMerge)
     749           1 :         || canMerge(mergeSorter, repo, mergeTip, toMerge);
     750             :   }
     751             : 
     752             :   public boolean hasMissingDependencies(MergeSorter mergeSorter, CodeReviewCommit toMerge) {
     753             :     try {
     754          23 :       return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
     755           0 :     } catch (IOException | StorageException e) {
     756           0 :       throw new StorageException("Branch head sorting failed", e);
     757             :     }
     758             :   }
     759             : 
     760             :   public CodeReviewCommit mergeOneCommit(
     761             :       PersonIdent author,
     762             :       PersonIdent committer,
     763             :       CodeReviewRevWalk rw,
     764             :       ObjectInserter inserter,
     765             :       Config repoConfig,
     766             :       BranchNameKey destBranch,
     767             :       CodeReviewCommit mergeTip,
     768             :       CodeReviewCommit n)
     769             :       throws InvalidMergeStrategyException {
     770          20 :     ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
     771             :     try {
     772          20 :       if (m.merge(mergeTip, n)) {
     773          20 :         return writeMergeCommit(
     774          20 :             author, committer, rw, inserter, destBranch, mergeTip, m.getResultTreeId(), n);
     775             :       }
     776           4 :       failed(rw, mergeTip, n, CommitMergeStatus.PATH_CONFLICT);
     777           0 :     } catch (NoMergeBaseException e) {
     778             :       try {
     779           0 :         failed(rw, mergeTip, n, getCommitMergeStatus(e.getReason()));
     780           0 :       } catch (IOException e2) {
     781           0 :         throw new StorageException("Cannot merge " + n.name(), e2);
     782           0 :       }
     783           0 :     } catch (IOException e) {
     784           0 :       throw new StorageException("Cannot merge " + n.name(), e);
     785           4 :     }
     786           4 :     return mergeTip;
     787             :   }
     788             : 
     789             :   private static CommitMergeStatus getCommitMergeStatus(MergeBaseFailureReason reason) {
     790           0 :     switch (reason) {
     791             :       case MULTIPLE_MERGE_BASES_NOT_SUPPORTED:
     792             :       case TOO_MANY_MERGE_BASES:
     793             :       default:
     794           0 :         return CommitMergeStatus.MANUAL_RECURSIVE_MERGE;
     795             :       case CONFLICTS_DURING_MERGE_BASE_CALCULATION:
     796           0 :         return CommitMergeStatus.PATH_CONFLICT;
     797             :     }
     798             :   }
     799             : 
     800             :   private static CodeReviewCommit failed(
     801             :       CodeReviewRevWalk rw,
     802             :       CodeReviewCommit mergeTip,
     803             :       CodeReviewCommit n,
     804             :       CommitMergeStatus failure)
     805             :       throws MissingObjectException, IncorrectObjectTypeException, IOException {
     806           4 :     rw.reset();
     807           4 :     rw.markStart(n);
     808           4 :     rw.markUninteresting(mergeTip);
     809             :     CodeReviewCommit failed;
     810           4 :     while ((failed = rw.next()) != null) {
     811           4 :       failed.setStatusCode(failure);
     812             :     }
     813           4 :     return failed;
     814             :   }
     815             : 
     816             :   public CodeReviewCommit writeMergeCommit(
     817             :       PersonIdent author,
     818             :       PersonIdent committer,
     819             :       CodeReviewRevWalk rw,
     820             :       ObjectInserter inserter,
     821             :       BranchNameKey destBranch,
     822             :       CodeReviewCommit mergeTip,
     823             :       ObjectId treeId,
     824             :       CodeReviewCommit n)
     825             :       throws IOException, MissingObjectException, IncorrectObjectTypeException {
     826          20 :     final List<CodeReviewCommit> merged = new ArrayList<>();
     827          20 :     rw.reset();
     828          20 :     rw.markStart(n);
     829          20 :     rw.markUninteresting(mergeTip);
     830             :     CodeReviewCommit crc;
     831          20 :     while ((crc = rw.next()) != null) {
     832          20 :       if (crc.getPatchsetId() != null) {
     833          20 :         merged.add(crc);
     834             :       }
     835             :     }
     836             : 
     837          20 :     StringBuilder msgbuf = new StringBuilder().append(summarize(rw, merged));
     838          20 :     if (!R_HEADS_MASTER.equals(destBranch.branch())) {
     839           3 :       msgbuf.append(" into ");
     840           3 :       msgbuf.append(destBranch.shortName());
     841             :     }
     842             : 
     843          20 :     if (merged.size() > 1) {
     844           5 :       msgbuf.append("\n\n* changes:\n");
     845           5 :       for (CodeReviewCommit c : merged) {
     846           5 :         rw.parseBody(c);
     847           5 :         msgbuf.append("  ");
     848           5 :         msgbuf.append(c.getShortMessage());
     849           5 :         msgbuf.append("\n");
     850           5 :       }
     851             :     }
     852             : 
     853          20 :     final CommitBuilder mergeCommit = new CommitBuilder();
     854          20 :     mergeCommit.setTreeId(treeId);
     855          20 :     mergeCommit.setParentIds(mergeTip, n);
     856          20 :     mergeCommit.setAuthor(author);
     857          20 :     mergeCommit.setCommitter(committer);
     858          20 :     mergeCommit.setMessage(msgbuf.toString());
     859             : 
     860          20 :     CodeReviewCommit mergeResult = rw.parseCommit(inserter.insert(mergeCommit));
     861          20 :     mergeResult.setNotes(n.getNotes());
     862          20 :     return mergeResult;
     863             :   }
     864             : 
     865             :   private String summarize(RevWalk rw, List<CodeReviewCommit> merged) throws IOException {
     866          20 :     if (merged.size() == 1) {
     867          20 :       CodeReviewCommit c = merged.get(0);
     868          20 :       rw.parseBody(c);
     869          20 :       return String.format("Merge \"%s\"", c.getShortMessage());
     870             :     }
     871             : 
     872           5 :     ImmutableSortedSet<String> topics =
     873           5 :         merged.stream()
     874           5 :             .map(c -> c.change().getTopic())
     875           5 :             .filter(t -> !Strings.isNullOrEmpty(t))
     876           5 :             .map(t -> "\"" + t + "\"")
     877           5 :             .collect(toImmutableSortedSet(naturalOrder()));
     878             : 
     879           5 :     if (!topics.isEmpty()) {
     880           2 :       return String.format(
     881             :           "Merge changes from topic%s %s",
     882           2 :           topics.size() > 1 ? "s" : "", topics.stream().collect(joining(", ")));
     883             :     }
     884           4 :     return merged.stream()
     885           4 :         .limit(5)
     886           4 :         .map(c -> c.change().getKey().abbreviate())
     887           4 :         .collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : ""));
     888             :   }
     889             : 
     890             :   public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig)
     891             :       throws InvalidMergeStrategyException {
     892          36 :     return newThreeWayMerger(inserter, repoConfig, mergeStrategyName());
     893             :   }
     894             : 
     895             :   public String mergeStrategyName() {
     896          40 :     return mergeStrategyName(useContentMerge, useRecursiveMerge);
     897             :   }
     898             : 
     899             :   public static String mergeStrategyName(boolean useContentMerge, boolean useRecursiveMerge) {
     900             :     String mergeStrategy;
     901             : 
     902          62 :     if (useContentMerge) {
     903             :       // Settings for this project allow us to try and automatically resolve
     904             :       // conflicts within files if needed. Use either the old resolve merger or
     905             :       // new recursive merger, and instruct to operate in core.
     906          62 :       if (useRecursiveMerge) {
     907          62 :         mergeStrategy = MergeStrategy.RECURSIVE.getName();
     908             :       } else {
     909           0 :         mergeStrategy = MergeStrategy.RESOLVE.getName();
     910             :       }
     911             :     } else {
     912             :       // No auto conflict resolving allowed. If any of the
     913             :       // affected files was modified, merge will fail.
     914           6 :       mergeStrategy = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.getName();
     915             :     }
     916             : 
     917          62 :     logger.atFine().log(
     918             :         "mergeStrategy = %s (useContentMerge = %s, useRecursiveMerge = %s)",
     919          62 :         mergeStrategy, useContentMerge, useRecursiveMerge);
     920          62 :     return mergeStrategy;
     921             :   }
     922             : 
     923             :   public static ThreeWayMerger newThreeWayMerger(
     924             :       ObjectInserter inserter, Config repoConfig, String strategyName)
     925             :       throws InvalidMergeStrategyException {
     926          54 :     Merger m = newMerger(inserter, repoConfig, strategyName);
     927          54 :     checkArgument(
     928             :         m instanceof ThreeWayMerger,
     929             :         "merge strategy %s does not support three-way merging",
     930             :         strategyName);
     931          54 :     return (ThreeWayMerger) m;
     932             :   }
     933             : 
     934             :   public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName)
     935             :       throws InvalidMergeStrategyException {
     936          56 :     MergeStrategy strategy = MergeStrategy.get(strategyName);
     937          56 :     if (strategy == null) {
     938           3 :       throw new InvalidMergeStrategyException(strategyName);
     939             :     }
     940          56 :     return strategy.newMerger(
     941          56 :         new ObjectInserter.Filter() {
     942             :           @Override
     943             :           protected ObjectInserter delegate() {
     944          56 :             return inserter;
     945             :           }
     946             : 
     947             :           @Override
     948          53 :           public void flush() {}
     949             : 
     950             :           @Override
     951          54 :           public void close() {}
     952             :         },
     953             :         repoConfig);
     954             :   }
     955             : 
     956             :   public void markCleanMerges(
     957             :       RevWalk rw, RevFlag canMergeFlag, CodeReviewCommit mergeTip, Set<RevCommit> alreadyAccepted) {
     958          53 :     if (mergeTip == null) {
     959             :       // If mergeTip is null here, branchTip was null, indicating a new branch
     960             :       // at the start of the merge process. We also elected to merge nothing,
     961             :       // probably due to missing dependencies. Nothing was cleanly merged.
     962             :       //
     963           0 :       return;
     964             :     }
     965             : 
     966             :     try {
     967          53 :       rw.resetRetain(canMergeFlag);
     968          53 :       rw.sort(RevSort.TOPO);
     969          53 :       rw.sort(RevSort.REVERSE, true);
     970          53 :       rw.markStart(mergeTip);
     971          53 :       for (RevCommit c : alreadyAccepted) {
     972             :         // If branch was not created by this submit.
     973          53 :         if (!Objects.equals(c, mergeTip)) {
     974          53 :           rw.markUninteresting(c);
     975             :         }
     976          53 :       }
     977             : 
     978             :       CodeReviewCommit c;
     979          53 :       while ((c = (CodeReviewCommit) rw.next()) != null) {
     980          53 :         if (c.getPatchsetId() != null && c.getStatusCode() == null) {
     981          53 :           c.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
     982             :         }
     983             :       }
     984           0 :     } catch (IOException e) {
     985           0 :       throw new StorageException("Cannot mark clean merges", e);
     986          53 :     }
     987          53 :   }
     988             : 
     989             :   public Set<Change.Id> findUnmergedChanges(
     990             :       Set<Change.Id> expected,
     991             :       CodeReviewRevWalk rw,
     992             :       RevFlag canMergeFlag,
     993             :       CodeReviewCommit oldTip,
     994             :       CodeReviewCommit mergeTip,
     995             :       Iterable<Change.Id> alreadyMerged) {
     996          53 :     if (mergeTip == null) {
     997           0 :       return expected;
     998             :     }
     999             : 
    1000             :     try {
    1001          53 :       Set<Change.Id> found = Sets.newHashSetWithExpectedSize(expected.size());
    1002          53 :       Iterables.addAll(found, alreadyMerged);
    1003          53 :       rw.resetRetain(canMergeFlag);
    1004          53 :       rw.sort(RevSort.TOPO);
    1005          53 :       rw.markStart(mergeTip);
    1006          53 :       if (oldTip != null) {
    1007          53 :         rw.markUninteresting(oldTip);
    1008             :       }
    1009             : 
    1010             :       CodeReviewCommit c;
    1011          53 :       while ((c = rw.next()) != null) {
    1012          53 :         if (c.getPatchsetId() == null) {
    1013          20 :           continue;
    1014             :         }
    1015          53 :         Change.Id id = c.getPatchsetId().changeId();
    1016          53 :         if (!expected.contains(id)) {
    1017           0 :           continue;
    1018             :         }
    1019          53 :         found.add(id);
    1020          53 :         if (found.size() == expected.size()) {
    1021          53 :           return Collections.emptySet();
    1022             :         }
    1023          15 :       }
    1024           6 :       return Sets.difference(expected, found);
    1025           0 :     } catch (IOException e) {
    1026           0 :       throw new StorageException("Cannot check if changes were merged", e);
    1027             :     }
    1028             :   }
    1029             : 
    1030             :   @Nullable
    1031             :   public static CodeReviewCommit findAnyMergedInto(
    1032             :       CodeReviewRevWalk rw, Iterable<CodeReviewCommit> commits, CodeReviewCommit tip)
    1033             :       throws IOException {
    1034          53 :     for (CodeReviewCommit c : commits) {
    1035             :       // TODO(dborowitz): Seems like this could get expensive for many patch
    1036             :       // sets. Is there a more efficient implementation?
    1037          53 :       if (rw.isMergedInto(c, tip)) {
    1038           7 :         return c;
    1039             :       }
    1040          53 :     }
    1041          53 :     return null;
    1042             :   }
    1043             : 
    1044             :   public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str)
    1045             :       throws BadRequestException, ResourceNotFoundException, IOException {
    1046             :     try {
    1047           4 :       ObjectId commitId = repo.resolve(str);
    1048           4 :       if (commitId == null) {
    1049           2 :         throw new BadRequestException("Cannot resolve '" + str + "' to a commit");
    1050             :       }
    1051           4 :       return rw.parseCommit(commitId);
    1052           0 :     } catch (AmbiguousObjectException | IncorrectObjectTypeException | RevisionSyntaxException e) {
    1053           0 :       throw new BadRequestException(e.getMessage());
    1054           0 :     } catch (MissingObjectException e) {
    1055           0 :       throw new ResourceNotFoundException(e.getMessage());
    1056             :     }
    1057             :   }
    1058             : 
    1059             :   private static void matchAuthorToCommitterDate(ProjectState project, CommitBuilder commit) {
    1060          14 :     if (project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE)) {
    1061           2 :       commit.setAuthor(
    1062             :           new PersonIdent(
    1063           2 :               commit.getAuthor(),
    1064           2 :               commit.getCommitter().getWhen(),
    1065           2 :               commit.getCommitter().getTimeZone()));
    1066             :     }
    1067          14 :   }
    1068             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750