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 static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;

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

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

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

  58.     private String ourCommitName = null;

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

  60.     private MergeResult failingResult;

  61.     private List<String> unmergedPaths;

  62.     private MergeStrategy strategy = MergeStrategy.RECURSIVE;

  63.     private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;

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

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

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

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

  96.             newHead = headCommit;

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

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

  112.                 RevCommit srcParent = srcCommit.getParent(0);
  113.                 revWalk.parseHeaders(srcParent);

  114.                 String ourName = calculateOurName(headRef);
  115.                 String revertName = srcCommit.getId()
  116.                         .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + " " //$NON-NLS-1$
  117.                         + srcCommit.getShortMessage();

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

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

  188.     /**
  189.      * Include a {@code Ref} to a commit to be reverted
  190.      *
  191.      * @param commit
  192.      *            a reference to a commit to be reverted into the current head
  193.      * @return {@code this}
  194.      */
  195.     public RevertCommand include(Ref commit) {
  196.         checkCallable();
  197.         commits.add(commit);
  198.         return this;
  199.     }

  200.     /**
  201.      * Include a commit to be reverted
  202.      *
  203.      * @param commit
  204.      *            the Id of a commit to be reverted into the current head
  205.      * @return {@code this}
  206.      */
  207.     public RevertCommand include(AnyObjectId commit) {
  208.         return include(commit.getName(), commit);
  209.     }

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

  223.     /**
  224.      * Set the name to be used in the "OURS" place for conflict markers
  225.      *
  226.      * @param ourCommitName
  227.      *            the name that should be used in the "OURS" place for conflict
  228.      *            markers
  229.      * @return {@code this}
  230.      */
  231.     public RevertCommand setOurCommitName(String ourCommitName) {
  232.         this.ourCommitName = ourCommitName;
  233.         return this;
  234.     }

  235.     private String calculateOurName(Ref headRef) {
  236.         if (ourCommitName != null)
  237.             return ourCommitName;

  238.         String targetRefName = headRef.getTarget().getName();
  239.         String headName = Repository.shortenRefName(targetRefName);
  240.         return headName;
  241.     }

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

  252.     /**
  253.      * Get the result of a merge failure
  254.      *
  255.      * @return the result of a merge failure, <code>null</code> if no merge
  256.      *         failure occurred during the revert
  257.      */
  258.     public MergeResult getFailingResult() {
  259.         return failingResult;
  260.     }

  261.     /**
  262.      * Get unmerged paths
  263.      *
  264.      * @return the unmerged paths, will be null if no merge conflicts
  265.      */
  266.     public List<String> getUnmergedPaths() {
  267.         return unmergedPaths;
  268.     }

  269.     /**
  270.      * Set the merge strategy to use for this revert command
  271.      *
  272.      * @param strategy
  273.      *            The merge strategy to use for this revert command.
  274.      * @return {@code this}
  275.      * @since 3.4
  276.      */
  277.     public RevertCommand setStrategy(MergeStrategy strategy) {
  278.         this.strategy = strategy;
  279.         return this;
  280.     }

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