StashApplyCommand.java

  1. /*
  2.  * Copyright (C) 2012, 2017 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.MergeStrategy;
  39. import org.eclipse.jgit.merge.ResolveMerger;
  40. import org.eclipse.jgit.revwalk.RevCommit;
  41. import org.eclipse.jgit.revwalk.RevTree;
  42. import org.eclipse.jgit.revwalk.RevWalk;
  43. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  44. import org.eclipse.jgit.treewalk.FileTreeIterator;
  45. import org.eclipse.jgit.treewalk.TreeWalk;

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

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

  58.     private String stashRef;

  59.     private boolean restoreIndex = true;

  60.     private boolean restoreUntracked = true;

  61.     private boolean ignoreRepositoryState;

  62.     private MergeStrategy strategy = MergeStrategy.RECURSIVE;

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

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

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

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

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

  123.         if (!ignoreRepositoryState
  124.                 && repo.getRepositoryState() != RepositoryState.SAFE)
  125.             throw new WrongRepositoryStateException(MessageFormat.format(
  126.                     JGitText.get().stashApplyOnUnsafeRepository,
  127.                     repo.getRepositoryState()));

  128.         try (ObjectReader reader = repo.newObjectReader();
  129.                 RevWalk revWalk = new RevWalk(reader)) {

  130.             ObjectId headCommit = repo.resolve(Constants.HEAD);
  131.             if (headCommit == null)
  132.                 throw new NoHeadException(JGitText.get().stashApplyWithoutHead);

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

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

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

  180.                 if (untrackedCommit != null) {
  181.                     ResolveMerger untrackedMerger = (ResolveMerger) strategy
  182.                             .newMerger(repo, true);
  183.                     untrackedMerger.setCommitNames(new String[] {
  184.                             "null", "HEAD", "untracked files" }); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
  185.                     // There is no common base for HEAD & untracked files
  186.                     // because the commit for untracked files has no parent. If
  187.                     // we use stashHeadCommit as common base (as in the other
  188.                     // merges) we potentially report conflicts for files
  189.                     // which are not even member of untracked files commit
  190.                     untrackedMerger.setBase(null);
  191.                     boolean ok = untrackedMerger.merge(headCommit,
  192.                             untrackedCommit);
  193.                     if (ok) {
  194.                         try {
  195.                             RevTree untrackedTree = revWalk
  196.                                     .parseTree(untrackedCommit);
  197.                             resetUntracked(untrackedTree);
  198.                         } catch (CheckoutConflictException e) {
  199.                             throw new StashApplyFailureException(
  200.                                     JGitText.get().stashApplyConflict, e);
  201.                         }
  202.                     } else {
  203.                         throw new StashApplyFailureException(
  204.                                 JGitText.get().stashApplyConflict);
  205.                     }
  206.                 }
  207.             } else {
  208.                 throw new StashApplyFailureException(
  209.                         JGitText.get().stashApplyConflict);
  210.             }
  211.             return stashId;

  212.         } catch (JGitInternalException e) {
  213.             throw e;
  214.         } catch (IOException e) {
  215.             throw new JGitInternalException(JGitText.get().stashApplyFailed, e);
  216.         }
  217.     }

  218.     /**
  219.      * Whether to restore the index state
  220.      *
  221.      * @param applyIndex
  222.      *            true (default) if the command should restore the index state
  223.      * @deprecated use {@link #setRestoreIndex} instead
  224.      */
  225.     @Deprecated
  226.     public void setApplyIndex(boolean applyIndex) {
  227.         this.restoreIndex = applyIndex;
  228.     }

  229.     /**
  230.      * Whether to restore the index state
  231.      *
  232.      * @param restoreIndex
  233.      *            true (default) if the command should restore the index state
  234.      * @return {@code this}
  235.      * @since 5.3
  236.      */
  237.     public StashApplyCommand setRestoreIndex(boolean restoreIndex) {
  238.         this.restoreIndex = restoreIndex;
  239.         return this;
  240.     }

  241.     /**
  242.      * Set the <code>MergeStrategy</code> to use.
  243.      *
  244.      * @param strategy
  245.      *            The merge strategy to use in order to merge during this
  246.      *            command execution.
  247.      * @return {@code this}
  248.      * @since 3.4
  249.      */
  250.     public StashApplyCommand setStrategy(MergeStrategy strategy) {
  251.         this.strategy = strategy;
  252.         return this;
  253.     }

  254.     /**
  255.      * Whether the command should restore untracked files
  256.      *
  257.      * @param applyUntracked
  258.      *            true (default) if the command should restore untracked files
  259.      * @since 3.4
  260.      * @deprecated use {@link #setRestoreUntracked} instead
  261.      */
  262.     @Deprecated
  263.     public void setApplyUntracked(boolean applyUntracked) {
  264.         this.restoreUntracked = applyUntracked;
  265.     }

  266.     /**
  267.      * Whether the command should restore untracked files
  268.      *
  269.      * @param restoreUntracked
  270.      *            true (default) if the command should restore untracked files
  271.      * @return {@code this}
  272.      * @since 5.3
  273.      */
  274.     public StashApplyCommand setRestoreUntracked(boolean restoreUntracked) {
  275.         this.restoreUntracked = restoreUntracked;
  276.         return this;
  277.     }

  278.     private void resetIndex(RevTree tree) throws IOException {
  279.         DirCache dc = repo.lockDirCache();
  280.         try (TreeWalk walk = new TreeWalk(repo)) {
  281.             DirCacheBuilder builder = dc.builder();

  282.             walk.addTree(tree);
  283.             walk.addTree(new DirCacheIterator(dc));
  284.             walk.setRecursive(true);

  285.             while (walk.next()) {
  286.                 AbstractTreeIterator cIter = walk.getTree(0,
  287.                         AbstractTreeIterator.class);
  288.                 if (cIter == null) {
  289.                     // Not in commit, don't add to new index
  290.                     continue;
  291.                 }

  292.                 final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
  293.                 entry.setFileMode(cIter.getEntryFileMode());
  294.                 entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());

  295.                 DirCacheIterator dcIter = walk.getTree(1,
  296.                         DirCacheIterator.class);
  297.                 if (dcIter != null && dcIter.idEqual(cIter)) {
  298.                     DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
  299.                     entry.setLastModified(indexEntry.getLastModifiedInstant());
  300.                     entry.setLength(indexEntry.getLength());
  301.                 }

  302.                 builder.add(entry);
  303.             }

  304.             builder.commit();
  305.         } finally {
  306.             dc.unlock();
  307.         }
  308.     }

  309.     private void resetUntracked(RevTree tree) throws CheckoutConflictException,
  310.             IOException {
  311.         Set<String> actuallyModifiedPaths = new HashSet<>();
  312.         // TODO maybe NameConflictTreeWalk ?
  313.         try (TreeWalk walk = new TreeWalk(repo)) {
  314.             walk.addTree(tree);
  315.             walk.addTree(new FileTreeIterator(repo));
  316.             walk.setRecursive(true);

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

  318.             while (walk.next()) {
  319.                 final AbstractTreeIterator cIter = walk.getTree(0,
  320.                         AbstractTreeIterator.class);
  321.                 if (cIter == null)
  322.                     // Not in commit, don't create untracked
  323.                     continue;

  324.                 final EolStreamType eolStreamType = walk
  325.                         .getEolStreamType(CHECKOUT_OP);
  326.                 final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
  327.                 entry.setFileMode(cIter.getEntryFileMode());
  328.                 entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());

  329.                 FileTreeIterator fIter = walk
  330.                         .getTree(1, FileTreeIterator.class);
  331.                 if (fIter != null) {
  332.                     if (fIter.isModified(entry, true, reader)) {
  333.                         // file exists and is dirty
  334.                         throw new CheckoutConflictException(
  335.                                 entry.getPathString());
  336.                     }
  337.                 }

  338.                 checkoutPath(entry, reader,
  339.                         new CheckoutMetadata(eolStreamType, null));
  340.                 actuallyModifiedPaths.add(entry.getPathString());
  341.             }
  342.         } finally {
  343.             if (!actuallyModifiedPaths.isEmpty()) {
  344.                 repo.fireEvent(new WorkingTreeModifiedEvent(
  345.                         actuallyModifiedPaths, null));
  346.             }
  347.         }
  348.     }

  349.     private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
  350.             CheckoutMetadata checkoutMetadata) {
  351.         try {
  352.             DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
  353.                     checkoutMetadata);
  354.         } catch (IOException e) {
  355.             throw new JGitInternalException(MessageFormat.format(
  356.                     JGitText.get().checkoutConflictWithFile,
  357.                     entry.getPathString()), e);
  358.         }
  359.     }
  360. }