StashApplyCommand.java

  1. /*
  2.  * Copyright (C) 2012, 2021 GitHub Inc. 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.treewalk.TreeWalk.OperationType.CHECKOUT_OP;

  12. import java.io.IOException;
  13. import java.text.MessageFormat;
  14. import java.util.HashSet;
  15. import java.util.List;
  16. import java.util.Set;

  17. import org.eclipse.jgit.api.errors.GitAPIException;
  18. import org.eclipse.jgit.api.errors.InvalidRefNameException;
  19. import org.eclipse.jgit.api.errors.JGitInternalException;
  20. import org.eclipse.jgit.api.errors.NoHeadException;
  21. import org.eclipse.jgit.api.errors.StashApplyFailureException;
  22. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  23. import org.eclipse.jgit.dircache.DirCache;
  24. import org.eclipse.jgit.dircache.DirCacheBuilder;
  25. import org.eclipse.jgit.dircache.DirCacheCheckout;
  26. import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
  27. import org.eclipse.jgit.dircache.DirCacheEntry;
  28. import org.eclipse.jgit.dircache.DirCacheIterator;
  29. import org.eclipse.jgit.errors.CheckoutConflictException;
  30. import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
  31. import org.eclipse.jgit.internal.JGitText;
  32. import org.eclipse.jgit.lib.Constants;
  33. import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
  34. import org.eclipse.jgit.lib.ObjectId;
  35. import org.eclipse.jgit.lib.ObjectReader;
  36. import org.eclipse.jgit.lib.Repository;
  37. import org.eclipse.jgit.lib.RepositoryState;
  38. import org.eclipse.jgit.merge.ContentMergeStrategy;
  39. import org.eclipse.jgit.merge.MergeStrategy;
  40. import org.eclipse.jgit.merge.Merger;
  41. import org.eclipse.jgit.merge.ResolveMerger;
  42. import org.eclipse.jgit.revwalk.RevCommit;
  43. import org.eclipse.jgit.revwalk.RevTree;
  44. import org.eclipse.jgit.revwalk.RevWalk;
  45. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  46. import org.eclipse.jgit.treewalk.FileTreeIterator;
  47. import org.eclipse.jgit.treewalk.TreeWalk;
  48. import org.eclipse.jgit.treewalk.WorkingTreeOptions;

  49. /**
  50.  * Command class to apply a stashed commit.
  51.  *
  52.  * This class behaves like <em>git stash apply --index</em>, i.e. it tries to
  53.  * recover the stashed index state in addition to the working tree state.
  54.  *
  55.  * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
  56.  *      >Git documentation about Stash</a>
  57.  * @since 2.0
  58.  */
  59. public class StashApplyCommand extends GitCommand<ObjectId> {

  60.     private static final String DEFAULT_REF = Constants.STASH + "@{0}"; //$NON-NLS-1$

  61.     private String stashRef;

  62.     private boolean restoreIndex = true;

  63.     private boolean restoreUntracked = true;

  64.     private boolean ignoreRepositoryState;

  65.     private MergeStrategy strategy = MergeStrategy.RECURSIVE;

  66.     private ContentMergeStrategy contentStrategy;

  67.     /**
  68.      * Create command to apply the changes of a stashed commit
  69.      *
  70.      * @param repo
  71.      *            the {@link org.eclipse.jgit.lib.Repository} to apply the stash
  72.      *            to
  73.      */
  74.     public StashApplyCommand(Repository repo) {
  75.         super(repo);
  76.     }

  77.     /**
  78.      * Set the stash reference to apply
  79.      * <p>
  80.      * This will default to apply the latest stashed commit (stash@{0}) if
  81.      * unspecified
  82.      *
  83.      * @param stashRef
  84.      *            name of the stash {@code Ref} to apply
  85.      * @return {@code this}
  86.      */
  87.     public StashApplyCommand setStashRef(String stashRef) {
  88.         this.stashRef = stashRef;
  89.         return this;
  90.     }

  91.     /**
  92.      * Whether to ignore the repository state when applying the stash
  93.      *
  94.      * @param willIgnoreRepositoryState
  95.      *            whether to ignore the repository state when applying the stash
  96.      * @return {@code this}
  97.      * @since 3.2
  98.      */
  99.     public StashApplyCommand ignoreRepositoryState(boolean willIgnoreRepositoryState) {
  100.         this.ignoreRepositoryState = willIgnoreRepositoryState;
  101.         return this;
  102.     }

  103.     private ObjectId getStashId() throws GitAPIException {
  104.         final String revision = stashRef != null ? stashRef : DEFAULT_REF;
  105.         final ObjectId stashId;
  106.         try {
  107.             stashId = repo.resolve(revision);
  108.         } catch (IOException e) {
  109.             throw new InvalidRefNameException(MessageFormat.format(
  110.                     JGitText.get().stashResolveFailed, revision), e);
  111.         }
  112.         if (stashId == null)
  113.             throw new InvalidRefNameException(MessageFormat.format(
  114.                     JGitText.get().stashResolveFailed, revision));
  115.         return stashId;
  116.     }

  117.     /**
  118.      * {@inheritDoc}
  119.      * <p>
  120.      * Apply the changes in a stashed commit to the working directory and index
  121.      */
  122.     @Override
  123.     public ObjectId call() throws GitAPIException,
  124.             WrongRepositoryStateException, NoHeadException,
  125.             StashApplyFailureException {
  126.         checkCallable();

  127.         if (!ignoreRepositoryState
  128.                 && repo.getRepositoryState() != RepositoryState.SAFE)
  129.             throw new WrongRepositoryStateException(MessageFormat.format(
  130.                     JGitText.get().stashApplyOnUnsafeRepository,
  131.                     repo.getRepositoryState()));

  132.         try (ObjectReader reader = repo.newObjectReader();
  133.                 RevWalk revWalk = new RevWalk(reader)) {

  134.             ObjectId headCommit = repo.resolve(Constants.HEAD);
  135.             if (headCommit == null)
  136.                 throw new NoHeadException(JGitText.get().stashApplyWithoutHead);

  137.             final ObjectId stashId = getStashId();
  138.             RevCommit stashCommit = revWalk.parseCommit(stashId);
  139.             if (stashCommit.getParentCount() < 2
  140.                     || stashCommit.getParentCount() > 3)
  141.                 throw new JGitInternalException(MessageFormat.format(
  142.                         JGitText.get().stashCommitIncorrectNumberOfParents,
  143.                         stashId.name(),
  144.                         Integer.valueOf(stashCommit.getParentCount())));

  145.             ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$
  146.             ObjectId stashIndexCommit = revWalk.parseCommit(stashCommit
  147.                     .getParent(1));
  148.             ObjectId stashHeadCommit = stashCommit.getParent(0);
  149.             ObjectId untrackedCommit = null;
  150.             if (restoreUntracked && stashCommit.getParentCount() == 3)
  151.                 untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2));

  152.             Merger merger = strategy.newMerger(repo);
  153.             boolean mergeSucceeded;
  154.             if (merger instanceof ResolveMerger) {
  155.                 ResolveMerger resolveMerger = (ResolveMerger) merger;
  156.                 resolveMerger
  157.                         .setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$
  158.                                 "stash" }); //$NON-NLS-1$
  159.                 resolveMerger.setBase(stashHeadCommit);
  160.                 resolveMerger
  161.                         .setWorkingTreeIterator(new FileTreeIterator(repo));
  162.                 resolveMerger.setContentMergeStrategy(contentStrategy);
  163.                 mergeSucceeded = resolveMerger.merge(headCommit, stashCommit);
  164.                 List<String> modifiedByMerge = resolveMerger.getModifiedFiles();
  165.                 if (!modifiedByMerge.isEmpty()) {
  166.                     repo.fireEvent(new WorkingTreeModifiedEvent(modifiedByMerge,
  167.                             null));
  168.                 }
  169.             } else {
  170.                 mergeSucceeded = merger.merge(headCommit, stashCommit);
  171.             }
  172.             if (mergeSucceeded) {
  173.                 DirCache dc = repo.lockDirCache();
  174.                 DirCacheCheckout dco = new DirCacheCheckout(repo, headTree,
  175.                         dc, merger.getResultTreeId());
  176.                 dco.setFailOnConflict(true);
  177.                 dco.checkout(); // Ignoring failed deletes....
  178.                 if (restoreIndex) {
  179.                     Merger ixMerger = strategy.newMerger(repo, true);
  180.                     if (ixMerger instanceof ResolveMerger) {
  181.                         ResolveMerger resolveMerger = (ResolveMerger) ixMerger;
  182.                         resolveMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$
  183.                                 "HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$
  184.                         resolveMerger.setBase(stashHeadCommit);
  185.                         resolveMerger.setContentMergeStrategy(contentStrategy);
  186.                     }
  187.                     boolean ok = ixMerger.merge(headCommit, stashIndexCommit);
  188.                     if (ok) {
  189.                         resetIndex(revWalk
  190.                                 .parseTree(ixMerger.getResultTreeId()));
  191.                     } else {
  192.                         throw new StashApplyFailureException(
  193.                                 JGitText.get().stashApplyConflict);
  194.                     }
  195.                 }

  196.                 if (untrackedCommit != null) {
  197.                     Merger untrackedMerger = strategy.newMerger(repo, true);
  198.                     if (untrackedMerger instanceof ResolveMerger) {
  199.                         ResolveMerger resolveMerger = (ResolveMerger) untrackedMerger;
  200.                         resolveMerger.setCommitNames(new String[] { "null", "HEAD", //$NON-NLS-1$//$NON-NLS-2$
  201.                                 "untracked files" }); //$NON-NLS-1$
  202.                         // There is no common base for HEAD & untracked files
  203.                         // because the commit for untracked files has no parent.
  204.                         // If we use stashHeadCommit as common base (as in the
  205.                         // other merges) we potentially report conflicts for
  206.                         // files which are not even member of untracked files
  207.                         // commit.
  208.                         resolveMerger.setBase(null);
  209.                         resolveMerger.setContentMergeStrategy(contentStrategy);
  210.                     }
  211.                     boolean ok = untrackedMerger.merge(headCommit,
  212.                             untrackedCommit);
  213.                     if (ok) {
  214.                         try {
  215.                             RevTree untrackedTree = revWalk
  216.                                     .parseTree(untrackedCommit);
  217.                             resetUntracked(untrackedTree);
  218.                         } catch (CheckoutConflictException e) {
  219.                             throw new StashApplyFailureException(
  220.                                     JGitText.get().stashApplyConflict, e);
  221.                         }
  222.                     } else {
  223.                         throw new StashApplyFailureException(
  224.                                 JGitText.get().stashApplyConflict);
  225.                     }
  226.                 }
  227.             } else {
  228.                 throw new StashApplyFailureException(
  229.                         JGitText.get().stashApplyConflict);
  230.             }
  231.             return stashId;

  232.         } catch (JGitInternalException e) {
  233.             throw e;
  234.         } catch (IOException e) {
  235.             throw new JGitInternalException(JGitText.get().stashApplyFailed, e);
  236.         }
  237.     }

  238.     /**
  239.      * Whether to restore the index state
  240.      *
  241.      * @param applyIndex
  242.      *            true (default) if the command should restore the index state
  243.      * @deprecated use {@link #setRestoreIndex} instead
  244.      */
  245.     @Deprecated
  246.     public void setApplyIndex(boolean applyIndex) {
  247.         this.restoreIndex = applyIndex;
  248.     }

  249.     /**
  250.      * Whether to restore the index state
  251.      *
  252.      * @param restoreIndex
  253.      *            true (default) if the command should restore the index state
  254.      * @return {@code this}
  255.      * @since 5.3
  256.      */
  257.     public StashApplyCommand setRestoreIndex(boolean restoreIndex) {
  258.         this.restoreIndex = restoreIndex;
  259.         return this;
  260.     }

  261.     /**
  262.      * Set the <code>MergeStrategy</code> to use.
  263.      *
  264.      * @param strategy
  265.      *            The merge strategy to use in order to merge during this
  266.      *            command execution.
  267.      * @return {@code this}
  268.      * @since 3.4
  269.      */
  270.     public StashApplyCommand setStrategy(MergeStrategy strategy) {
  271.         this.strategy = strategy;
  272.         return this;
  273.     }

  274.     /**
  275.      * Sets the content merge strategy to use if the
  276.      * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
  277.      * "recursive".
  278.      *
  279.      * @param strategy
  280.      *            the {@link ContentMergeStrategy} to be used
  281.      * @return {@code this}
  282.      * @since 5.12
  283.      */
  284.     public StashApplyCommand setContentMergeStrategy(
  285.             ContentMergeStrategy strategy) {
  286.         checkCallable();
  287.         this.contentStrategy = strategy;
  288.         return this;
  289.     }

  290.     /**
  291.      * Whether the command should restore untracked files
  292.      *
  293.      * @param applyUntracked
  294.      *            true (default) if the command should restore untracked files
  295.      * @since 3.4
  296.      * @deprecated use {@link #setRestoreUntracked} instead
  297.      */
  298.     @Deprecated
  299.     public void setApplyUntracked(boolean applyUntracked) {
  300.         this.restoreUntracked = applyUntracked;
  301.     }

  302.     /**
  303.      * Whether the command should restore untracked files
  304.      *
  305.      * @param restoreUntracked
  306.      *            true (default) if the command should restore untracked files
  307.      * @return {@code this}
  308.      * @since 5.3
  309.      */
  310.     public StashApplyCommand setRestoreUntracked(boolean restoreUntracked) {
  311.         this.restoreUntracked = restoreUntracked;
  312.         return this;
  313.     }

  314.     private void resetIndex(RevTree tree) throws IOException {
  315.         DirCache dc = repo.lockDirCache();
  316.         try (TreeWalk walk = new TreeWalk(repo)) {
  317.             DirCacheBuilder builder = dc.builder();

  318.             walk.addTree(tree);
  319.             walk.addTree(new DirCacheIterator(dc));
  320.             walk.setRecursive(true);

  321.             while (walk.next()) {
  322.                 AbstractTreeIterator cIter = walk.getTree(0,
  323.                         AbstractTreeIterator.class);
  324.                 if (cIter == null) {
  325.                     // Not in commit, don't add to new index
  326.                     continue;
  327.                 }

  328.                 final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
  329.                 entry.setFileMode(cIter.getEntryFileMode());
  330.                 entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());

  331.                 DirCacheIterator dcIter = walk.getTree(1,
  332.                         DirCacheIterator.class);
  333.                 if (dcIter != null && dcIter.idEqual(cIter)) {
  334.                     DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
  335.                     entry.setLastModified(indexEntry.getLastModifiedInstant());
  336.                     entry.setLength(indexEntry.getLength());
  337.                 }

  338.                 builder.add(entry);
  339.             }

  340.             builder.commit();
  341.         } finally {
  342.             dc.unlock();
  343.         }
  344.     }

  345.     private void resetUntracked(RevTree tree) throws CheckoutConflictException,
  346.             IOException {
  347.         Set<String> actuallyModifiedPaths = new HashSet<>();
  348.         WorkingTreeOptions options = repo.getConfig()
  349.                 .get(WorkingTreeOptions.KEY);
  350.         // TODO maybe NameConflictTreeWalk ?
  351.         try (TreeWalk walk = new TreeWalk(repo)) {
  352.             walk.addTree(tree);
  353.             walk.addTree(new FileTreeIterator(repo));
  354.             walk.setRecursive(true);

  355.             final ObjectReader reader = walk.getObjectReader();

  356.             while (walk.next()) {
  357.                 final AbstractTreeIterator cIter = walk.getTree(0,
  358.                         AbstractTreeIterator.class);
  359.                 if (cIter == null)
  360.                     // Not in commit, don't create untracked
  361.                     continue;

  362.                 final EolStreamType eolStreamType = walk
  363.                         .getEolStreamType(CHECKOUT_OP);
  364.                 final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
  365.                 entry.setFileMode(cIter.getEntryFileMode());
  366.                 entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());

  367.                 FileTreeIterator fIter = walk
  368.                         .getTree(1, FileTreeIterator.class);
  369.                 if (fIter != null) {
  370.                     if (fIter.isModified(entry, true, reader)) {
  371.                         // file exists and is dirty
  372.                         throw new CheckoutConflictException(
  373.                                 entry.getPathString());
  374.                     }
  375.                 }

  376.                 checkoutPath(entry, reader, options,
  377.                         new CheckoutMetadata(eolStreamType, null));
  378.                 actuallyModifiedPaths.add(entry.getPathString());
  379.             }
  380.         } finally {
  381.             if (!actuallyModifiedPaths.isEmpty()) {
  382.                 repo.fireEvent(new WorkingTreeModifiedEvent(
  383.                         actuallyModifiedPaths, null));
  384.             }
  385.         }
  386.     }

  387.     private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
  388.             WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) {
  389.         try {
  390.             DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
  391.                     checkoutMetadata, options);
  392.         } catch (IOException e) {
  393.             throw new JGitInternalException(MessageFormat.format(
  394.                     JGitText.get().checkoutConflictWithFile,
  395.                     entry.getPathString()), e);
  396.         }
  397.     }
  398. }