RecursiveMerger.java

  1. /*
  2.  * Copyright (C) 2012, Research In Motion Limited
  3.  * Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */

  11. /*
  12.  * Contributors:
  13.  *    George Young - initial API and implementation
  14.  *    Christian Halstrick - initial API and implementation
  15.  */
  16. package org.eclipse.jgit.merge;

  17. import java.io.IOException;
  18. import java.text.MessageFormat;
  19. import java.util.ArrayList;
  20. import java.util.Date;
  21. import java.util.List;
  22. import java.util.TimeZone;

  23. import org.eclipse.jgit.dircache.DirCache;
  24. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  25. import org.eclipse.jgit.errors.NoMergeBaseException;
  26. import org.eclipse.jgit.internal.JGitText;
  27. import org.eclipse.jgit.lib.CommitBuilder;
  28. import org.eclipse.jgit.lib.Config;
  29. import org.eclipse.jgit.lib.ObjectId;
  30. import org.eclipse.jgit.lib.ObjectInserter;
  31. import org.eclipse.jgit.lib.PersonIdent;
  32. import org.eclipse.jgit.lib.Repository;
  33. import org.eclipse.jgit.revwalk.RevCommit;
  34. import org.eclipse.jgit.revwalk.filter.RevFilter;
  35. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  36. import org.eclipse.jgit.treewalk.EmptyTreeIterator;
  37. import org.eclipse.jgit.treewalk.WorkingTreeIterator;

  38. /**
  39.  * A three-way merger performing a content-merge if necessary across multiple
  40.  * bases using recursion
  41.  *
  42.  * This merger extends the resolve merger and does several things differently:
  43.  *
  44.  * - allow more than one merge base, up to a maximum
  45.  *
  46.  * - uses "Lists" instead of Arrays for chained types
  47.  *
  48.  * - recursively merges the merge bases together to compute a usable base
  49.  *
  50.  * @since 3.0
  51.  */
  52. public class RecursiveMerger extends ResolveMerger {
  53.     /**
  54.      * The maximum number of merge bases. This merge will stop when the number
  55.      * of merge bases exceeds this value
  56.      */
  57.     public final int MAX_BASES = 200;

  58.     /**
  59.      * Normal recursive merge when you want a choice of DirCache placement
  60.      * inCore
  61.      *
  62.      * @param local
  63.      *            a {@link org.eclipse.jgit.lib.Repository} object.
  64.      * @param inCore
  65.      *            a boolean.
  66.      */
  67.     protected RecursiveMerger(Repository local, boolean inCore) {
  68.         super(local, inCore);
  69.     }

  70.     /**
  71.      * Normal recursive merge, implies not inCore
  72.      *
  73.      * @param local a {@link org.eclipse.jgit.lib.Repository} object.
  74.      */
  75.     protected RecursiveMerger(Repository local) {
  76.         this(local, false);
  77.     }

  78.     /**
  79.      * Normal recursive merge, implies inCore.
  80.      *
  81.      * @param inserter
  82.      *            an {@link org.eclipse.jgit.lib.ObjectInserter} object.
  83.      * @param config
  84.      *            the repository configuration
  85.      * @since 4.8
  86.      */
  87.     protected RecursiveMerger(ObjectInserter inserter, Config config) {
  88.         super(inserter, config);
  89.     }

  90.     /**
  91.      * {@inheritDoc}
  92.      * <p>
  93.      * Get a single base commit for two given commits. If the two source commits
  94.      * have more than one base commit recursively merge the base commits
  95.      * together until you end up with a single base commit.
  96.      */
  97.     @Override
  98.     protected RevCommit getBaseCommit(RevCommit a, RevCommit b)
  99.             throws IncorrectObjectTypeException, IOException {
  100.         return getBaseCommit(a, b, 0);
  101.     }

  102.     /**
  103.      * Get a single base commit for two given commits. If the two source commits
  104.      * have more than one base commit recursively merge the base commits
  105.      * together until a virtual common base commit has been found.
  106.      *
  107.      * @param a
  108.      *            the first commit to be merged
  109.      * @param b
  110.      *            the second commit to be merged
  111.      * @param callDepth
  112.      *            the callDepth when this method is called recursively
  113.      * @return the merge base of two commits. If a criss-cross merge required a
  114.      *         synthetic merge base this commit is visible only the merger's
  115.      *         RevWalk and will not be in the repository.
  116.      * @throws java.io.IOException
  117.      * @throws IncorrectObjectTypeException
  118.      *             one of the input objects is not a commit.
  119.      * @throws NoMergeBaseException
  120.      *             too many merge bases are found or the computation of a common
  121.      *             merge base failed (e.g. because of a conflict).
  122.      */
  123.     protected RevCommit getBaseCommit(RevCommit a, RevCommit b, int callDepth)
  124.             throws IOException {
  125.         ArrayList<RevCommit> baseCommits = new ArrayList<>();
  126.         walk.reset();
  127.         walk.setRevFilter(RevFilter.MERGE_BASE);
  128.         walk.markStart(a);
  129.         walk.markStart(b);
  130.         RevCommit c;
  131.         while ((c = walk.next()) != null)
  132.             baseCommits.add(c);

  133.         if (baseCommits.isEmpty())
  134.             return null;
  135.         if (baseCommits.size() == 1)
  136.             return baseCommits.get(0);
  137.         if (baseCommits.size() >= MAX_BASES)
  138.             throw new NoMergeBaseException(NoMergeBaseException.MergeBaseFailureReason.TOO_MANY_MERGE_BASES, MessageFormat.format(
  139.                     JGitText.get().mergeRecursiveTooManyMergeBasesFor,
  140.                     Integer.valueOf(MAX_BASES), a.name(), b.name(),
  141.                             Integer.valueOf(baseCommits.size())));

  142.         // We know we have more than one base commit. We have to do merges now
  143.         // to determine a single base commit. We don't want to spoil the current
  144.         // dircache and working tree with the results of this intermediate
  145.         // merges. Therefore set the dircache to a new in-memory dircache and
  146.         // disable that we update the working-tree. We set this back to the
  147.         // original values once a single base commit is created.
  148.         RevCommit currentBase = baseCommits.get(0);
  149.         DirCache oldDircache = dircache;
  150.         boolean oldIncore = inCore;
  151.         WorkingTreeIterator oldWTreeIt = workingTreeIterator;
  152.         workingTreeIterator = null;
  153.         try {
  154.             dircache = DirCache.read(reader, currentBase.getTree());
  155.             inCore = true;

  156.             List<RevCommit> parents = new ArrayList<>();
  157.             parents.add(currentBase);
  158.             for (int commitIdx = 1; commitIdx < baseCommits.size(); commitIdx++) {
  159.                 RevCommit nextBase = baseCommits.get(commitIdx);
  160.                 if (commitIdx >= MAX_BASES)
  161.                     throw new NoMergeBaseException(
  162.                             NoMergeBaseException.MergeBaseFailureReason.TOO_MANY_MERGE_BASES,
  163.                             MessageFormat.format(
  164.                             JGitText.get().mergeRecursiveTooManyMergeBasesFor,
  165.                             Integer.valueOf(MAX_BASES), a.name(), b.name(),
  166.                                     Integer.valueOf(baseCommits.size())));
  167.                 parents.add(nextBase);
  168.                 RevCommit bc = getBaseCommit(currentBase, nextBase,
  169.                         callDepth + 1);
  170.                 AbstractTreeIterator bcTree = (bc == null) ? new EmptyTreeIterator()
  171.                         : openTree(bc.getTree());
  172.                 if (mergeTrees(bcTree, currentBase.getTree(),
  173.                         nextBase.getTree(), true))
  174.                     currentBase = createCommitForTree(resultTree, parents);
  175.                 else
  176.                     throw new NoMergeBaseException(
  177.                             NoMergeBaseException.MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION,
  178.                             MessageFormat.format(
  179.                                     JGitText.get().mergeRecursiveConflictsWhenMergingCommonAncestors,
  180.                                     currentBase.getName(), nextBase.getName()));
  181.             }
  182.         } finally {
  183.             inCore = oldIncore;
  184.             dircache = oldDircache;
  185.             workingTreeIterator = oldWTreeIt;
  186.             unmergedPaths.clear();
  187.             mergeResults.clear();
  188.             failingPaths.clear();
  189.         }
  190.         return currentBase;
  191.     }

  192.     /**
  193.      * Create a new commit by explicitly specifying the content tree and the
  194.      * parents. The commit message is not set and author/committer are set to
  195.      * the current user.
  196.      *
  197.      * @param tree
  198.      *            the tree this commit should capture
  199.      * @param parents
  200.      *            the list of parent commits
  201.      * @return a new commit visible only within this merger's RevWalk.
  202.      * @throws IOException
  203.      */
  204.     private RevCommit createCommitForTree(ObjectId tree, List<RevCommit> parents)
  205.             throws IOException {
  206.         CommitBuilder c = new CommitBuilder();
  207.         c.setTreeId(tree);
  208.         c.setParentIds(parents);
  209.         c.setAuthor(mockAuthor(parents));
  210.         c.setCommitter(c.getAuthor());
  211.         return RevCommit.parse(walk, c.build());
  212.     }

  213.     private static PersonIdent mockAuthor(List<RevCommit> parents) {
  214.         String name = RecursiveMerger.class.getSimpleName();
  215.         int time = 0;
  216.         for (RevCommit p : parents)
  217.             time = Math.max(time, p.getCommitTime());
  218.         return new PersonIdent(
  219.                 name, name + "@JGit", //$NON-NLS-1$
  220.                 new Date((time + 1) * 1000L),
  221.                 TimeZone.getTimeZone("GMT+0000")); //$NON-NLS-1$
  222.     }
  223. }