RevertCommand.java

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

  11. import java.io.IOException;
  12. import java.text.MessageFormat;
  13. import java.util.LinkedList;
  14. import java.util.List;
  15. import java.util.Map;

  16. import org.eclipse.jgit.api.MergeResult.MergeStatus;
  17. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  18. import org.eclipse.jgit.api.errors.GitAPIException;
  19. import org.eclipse.jgit.api.errors.JGitInternalException;
  20. import org.eclipse.jgit.api.errors.MultipleParentsNotAllowedException;
  21. import org.eclipse.jgit.api.errors.NoHeadException;
  22. import org.eclipse.jgit.api.errors.NoMessageException;
  23. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  24. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  25. import org.eclipse.jgit.dircache.DirCacheCheckout;
  26. import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
  27. import org.eclipse.jgit.internal.JGitText;
  28. import org.eclipse.jgit.lib.AnyObjectId;
  29. import org.eclipse.jgit.lib.Constants;
  30. import org.eclipse.jgit.lib.NullProgressMonitor;
  31. import org.eclipse.jgit.lib.ObjectId;
  32. import org.eclipse.jgit.lib.ObjectIdRef;
  33. import org.eclipse.jgit.lib.ProgressMonitor;
  34. import org.eclipse.jgit.lib.Ref;
  35. import org.eclipse.jgit.lib.Ref.Storage;
  36. import org.eclipse.jgit.lib.Repository;
  37. import org.eclipse.jgit.merge.MergeMessageFormatter;
  38. import org.eclipse.jgit.merge.MergeStrategy;
  39. import org.eclipse.jgit.merge.ResolveMerger;
  40. import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
  41. import org.eclipse.jgit.revwalk.RevCommit;
  42. import org.eclipse.jgit.revwalk.RevWalk;
  43. import org.eclipse.jgit.treewalk.FileTreeIterator;

  44. /**
  45.  * A class used to execute a {@code revert} command. It has setters for all
  46.  * supported options and arguments of this command and a {@link #call()} method
  47.  * to finally execute the command. Each instance of this class should only be
  48.  * used for one invocation of the command (means: one call to {@link #call()})
  49.  *
  50.  * @see <a
  51.  *      href="http://www.kernel.org/pub/software/scm/git/docs/git-revert.html"
  52.  *      >Git documentation about revert</a>
  53.  */
  54. public class RevertCommand extends GitCommand<RevCommit> {
  55.     private List<Ref> commits = new LinkedList<>();

  56.     private String ourCommitName = null;

  57.     private List<Ref> revertedRefs = new LinkedList<>();

  58.     private MergeResult failingResult;

  59.     private List<String> unmergedPaths;

  60.     private MergeStrategy strategy = MergeStrategy.RECURSIVE;

  61.     private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;

  62.     /**
  63.      * <p>
  64.      * Constructor for RevertCommand.
  65.      * </p>
  66.      *
  67.      * @param repo
  68.      *            the {@link org.eclipse.jgit.lib.Repository}
  69.      */
  70.     protected RevertCommand(Repository repo) {
  71.         super(repo);
  72.     }

  73.     /**
  74.      * {@inheritDoc}
  75.      * <p>
  76.      * Executes the {@code revert} command with all the options and parameters
  77.      * collected by the setter methods (e.g. {@link #include(Ref)} of this
  78.      * class. Each instance of this class should only be used for one invocation
  79.      * of the command. Don't call this method twice on an instance.
  80.      */
  81.     @Override
  82.     public RevCommit call() throws NoMessageException, UnmergedPathsException,
  83.             ConcurrentRefUpdateException, WrongRepositoryStateException,
  84.             GitAPIException {
  85.         RevCommit newHead = null;
  86.         checkCallable();

  87.         try (RevWalk revWalk = new RevWalk(repo)) {

  88.             // get the head commit
  89.             Ref headRef = repo.exactRef(Constants.HEAD);
  90.             if (headRef == null)
  91.                 throw new NoHeadException(
  92.                         JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
  93.             RevCommit headCommit = revWalk.parseCommit(headRef.getObjectId());

  94.             newHead = headCommit;

  95.             // loop through all refs to be reverted
  96.             for (Ref src : commits) {
  97.                 // get the commit to be reverted
  98.                 // handle annotated tags
  99.                 ObjectId srcObjectId = src.getPeeledObjectId();
  100.                 if (srcObjectId == null)
  101.                     srcObjectId = src.getObjectId();
  102.                 RevCommit srcCommit = revWalk.parseCommit(srcObjectId);

  103.                 // get the parent of the commit to revert
  104.                 if (srcCommit.getParentCount() != 1)
  105.                     throw new MultipleParentsNotAllowedException(
  106.                             MessageFormat.format(
  107.                                     JGitText.get().canOnlyRevertCommitsWithOneParent,
  108.                                     srcCommit.name(),
  109.                                     Integer.valueOf(srcCommit.getParentCount())));

  110.                 RevCommit srcParent = srcCommit.getParent(0);
  111.                 revWalk.parseHeaders(srcParent);

  112.                 String ourName = calculateOurName(headRef);
  113.                 String revertName = srcCommit.getId().abbreviate(7).name()
  114.                         + " " + srcCommit.getShortMessage(); //$NON-NLS-1$

  115.                 ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
  116.                 merger.setWorkingTreeIterator(new FileTreeIterator(repo));
  117.                 merger.setBase(srcCommit.getTree());
  118.                 merger.setCommitNames(new String[] {
  119.                         "BASE", ourName, revertName }); //$NON-NLS-1$

  120.                 String shortMessage = "Revert \"" + srcCommit.getShortMessage() //$NON-NLS-1$
  121.                         + "\""; //$NON-NLS-1$
  122.                 String newMessage = shortMessage + "\n\n" //$NON-NLS-1$
  123.                         + "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$
  124.                         + ".\n"; //$NON-NLS-1$
  125.                 if (merger.merge(headCommit, srcParent)) {
  126.                     if (!merger.getModifiedFiles().isEmpty()) {
  127.                         repo.fireEvent(new WorkingTreeModifiedEvent(
  128.                                 merger.getModifiedFiles(), null));
  129.                     }
  130.                     if (AnyObjectId.isEqual(headCommit.getTree().getId(),
  131.                             merger.getResultTreeId()))
  132.                         continue;
  133.                     DirCacheCheckout dco = new DirCacheCheckout(repo,
  134.                             headCommit.getTree(), repo.lockDirCache(),
  135.                             merger.getResultTreeId());
  136.                     dco.setFailOnConflict(true);
  137.                     dco.setProgressMonitor(monitor);
  138.                     dco.checkout();
  139.                     try (Git git = new Git(getRepository())) {
  140.                         newHead = git.commit().setMessage(newMessage)
  141.                                 .setReflogComment("revert: " + shortMessage) //$NON-NLS-1$
  142.                                 .call();
  143.                     }
  144.                     revertedRefs.add(src);
  145.                     headCommit = newHead;
  146.                 } else {
  147.                     unmergedPaths = merger.getUnmergedPaths();
  148.                     Map<String, MergeFailureReason> failingPaths = merger
  149.                             .getFailingPaths();
  150.                     if (failingPaths != null)
  151.                         failingResult = new MergeResult(null,
  152.                                 merger.getBaseCommitId(),
  153.                                 new ObjectId[] { headCommit.getId(),
  154.                                         srcParent.getId() },
  155.                                 MergeStatus.FAILED, strategy,
  156.                                 merger.getMergeResults(), failingPaths, null);
  157.                     else
  158.                         failingResult = new MergeResult(null,
  159.                                 merger.getBaseCommitId(),
  160.                                 new ObjectId[] { headCommit.getId(),
  161.                                         srcParent.getId() },
  162.                                 MergeStatus.CONFLICTING, strategy,
  163.                                 merger.getMergeResults(), failingPaths, null);
  164.                     if (!merger.failed() && !unmergedPaths.isEmpty()) {
  165.                         String message = new MergeMessageFormatter()
  166.                         .formatWithConflicts(newMessage,
  167.                                 merger.getUnmergedPaths());
  168.                         repo.writeRevertHead(srcCommit.getId());
  169.                         repo.writeMergeCommitMsg(message);
  170.                     }
  171.                     return null;
  172.                 }
  173.             }
  174.         } catch (IOException e) {
  175.             throw new JGitInternalException(
  176.                     MessageFormat.format(
  177.                                     JGitText.get().exceptionCaughtDuringExecutionOfRevertCommand,
  178.                             e), e);
  179.         }
  180.         return newHead;
  181.     }

  182.     /**
  183.      * Include a {@code Ref} to a commit to be reverted
  184.      *
  185.      * @param commit
  186.      *            a reference to a commit to be reverted into the current head
  187.      * @return {@code this}
  188.      */
  189.     public RevertCommand include(Ref commit) {
  190.         checkCallable();
  191.         commits.add(commit);
  192.         return this;
  193.     }

  194.     /**
  195.      * Include a commit to be reverted
  196.      *
  197.      * @param commit
  198.      *            the Id of a commit to be reverted into the current head
  199.      * @return {@code this}
  200.      */
  201.     public RevertCommand include(AnyObjectId commit) {
  202.         return include(commit.getName(), commit);
  203.     }

  204.     /**
  205.      * Include a commit to be reverted
  206.      *
  207.      * @param name
  208.      *            name of a {@code Ref} referring to the commit
  209.      * @param commit
  210.      *            the Id of a commit which is reverted into the current head
  211.      * @return {@code this}
  212.      */
  213.     public RevertCommand include(String name, AnyObjectId commit) {
  214.         return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name,
  215.                 commit.copy()));
  216.     }

  217.     /**
  218.      * Set the name to be used in the "OURS" place for conflict markers
  219.      *
  220.      * @param ourCommitName
  221.      *            the name that should be used in the "OURS" place for conflict
  222.      *            markers
  223.      * @return {@code this}
  224.      */
  225.     public RevertCommand setOurCommitName(String ourCommitName) {
  226.         this.ourCommitName = ourCommitName;
  227.         return this;
  228.     }

  229.     private String calculateOurName(Ref headRef) {
  230.         if (ourCommitName != null)
  231.             return ourCommitName;

  232.         String targetRefName = headRef.getTarget().getName();
  233.         String headName = Repository.shortenRefName(targetRefName);
  234.         return headName;
  235.     }

  236.     /**
  237.      * Get the list of successfully reverted {@link org.eclipse.jgit.lib.Ref}'s.
  238.      *
  239.      * @return the list of successfully reverted
  240.      *         {@link org.eclipse.jgit.lib.Ref}'s. Never <code>null</code> but
  241.      *         maybe an empty list if no commit was successfully cherry-picked
  242.      */
  243.     public List<Ref> getRevertedRefs() {
  244.         return revertedRefs;
  245.     }

  246.     /**
  247.      * Get the result of a merge failure
  248.      *
  249.      * @return the result of a merge failure, <code>null</code> if no merge
  250.      *         failure occurred during the revert
  251.      */
  252.     public MergeResult getFailingResult() {
  253.         return failingResult;
  254.     }

  255.     /**
  256.      * Get unmerged paths
  257.      *
  258.      * @return the unmerged paths, will be null if no merge conflicts
  259.      */
  260.     public List<String> getUnmergedPaths() {
  261.         return unmergedPaths;
  262.     }

  263.     /**
  264.      * Set the merge strategy to use for this revert command
  265.      *
  266.      * @param strategy
  267.      *            The merge strategy to use for this revert command.
  268.      * @return {@code this}
  269.      * @since 3.4
  270.      */
  271.     public RevertCommand setStrategy(MergeStrategy strategy) {
  272.         this.strategy = strategy;
  273.         return this;
  274.     }

  275.     /**
  276.      * The progress monitor associated with the revert operation. By default,
  277.      * this is set to <code>NullProgressMonitor</code>
  278.      *
  279.      * @see NullProgressMonitor
  280.      * @param monitor
  281.      *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
  282.      * @return {@code this}
  283.      * @since 4.11
  284.      */
  285.     public RevertCommand setProgressMonitor(ProgressMonitor monitor) {
  286.         if (monitor == null) {
  287.             monitor = NullProgressMonitor.INSTANCE;
  288.         }
  289.         this.monitor = monitor;
  290.         return this;
  291.     }
  292. }