StashCreateCommand.java

  1. /*
  2.  * Copyright (C) 2012, 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.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;

  12. import java.io.File;
  13. import java.io.IOException;
  14. import java.io.InputStream;
  15. import java.text.MessageFormat;
  16. import java.util.ArrayList;
  17. import java.util.List;

  18. import org.eclipse.jgit.api.ResetCommand.ResetType;
  19. import org.eclipse.jgit.api.errors.GitAPIException;
  20. import org.eclipse.jgit.api.errors.JGitInternalException;
  21. import org.eclipse.jgit.api.errors.NoHeadException;
  22. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  23. import org.eclipse.jgit.dircache.DirCache;
  24. import org.eclipse.jgit.dircache.DirCacheBuilder;
  25. import org.eclipse.jgit.dircache.DirCacheEditor;
  26. import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
  27. import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
  28. import org.eclipse.jgit.dircache.DirCacheEntry;
  29. import org.eclipse.jgit.dircache.DirCacheIterator;
  30. import org.eclipse.jgit.errors.UnmergedPathException;
  31. import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
  32. import org.eclipse.jgit.internal.JGitText;
  33. import org.eclipse.jgit.lib.CommitBuilder;
  34. import org.eclipse.jgit.lib.Constants;
  35. import org.eclipse.jgit.lib.MutableObjectId;
  36. import org.eclipse.jgit.lib.ObjectId;
  37. import org.eclipse.jgit.lib.ObjectInserter;
  38. import org.eclipse.jgit.lib.ObjectReader;
  39. import org.eclipse.jgit.lib.PersonIdent;
  40. import org.eclipse.jgit.lib.Ref;
  41. import org.eclipse.jgit.lib.RefUpdate;
  42. import org.eclipse.jgit.lib.Repository;
  43. import org.eclipse.jgit.revwalk.RevCommit;
  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.WorkingTreeIterator;
  49. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  50. import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
  51. import org.eclipse.jgit.treewalk.filter.SkipWorkTreeFilter;
  52. import org.eclipse.jgit.util.FileUtils;

  53. /**
  54.  * Command class to stash changes in the working directory and index in a
  55.  * commit.
  56.  *
  57.  * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
  58.  *      >Git documentation about Stash</a>
  59.  * @since 2.0
  60.  */
  61. public class StashCreateCommand extends GitCommand<RevCommit> {

  62.     private static final String MSG_INDEX = "index on {0}: {1} {2}"; //$NON-NLS-1$

  63.     private static final String MSG_UNTRACKED = "untracked files on {0}: {1} {2}"; //$NON-NLS-1$

  64.     private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}"; //$NON-NLS-1$

  65.     private String indexMessage = MSG_INDEX;

  66.     private String workingDirectoryMessage = MSG_WORKING_DIR;

  67.     private String ref = Constants.R_STASH;

  68.     private PersonIdent person;

  69.     private boolean includeUntracked;

  70.     /**
  71.      * Create a command to stash changes in the working directory and index
  72.      *
  73.      * @param repo
  74.      *            a {@link org.eclipse.jgit.lib.Repository} object.
  75.      */
  76.     public StashCreateCommand(Repository repo) {
  77.         super(repo);
  78.         person = new PersonIdent(repo);
  79.     }

  80.     /**
  81.      * Set the message used when committing index changes
  82.      * <p>
  83.      * The message will be formatted with the current branch, abbreviated commit
  84.      * id, and short commit message when used.
  85.      *
  86.      * @param message
  87.      *            the stash message
  88.      * @return {@code this}
  89.      */
  90.     public StashCreateCommand setIndexMessage(String message) {
  91.         indexMessage = message;
  92.         return this;
  93.     }

  94.     /**
  95.      * Set the message used when committing working directory changes
  96.      * <p>
  97.      * The message will be formatted with the current branch, abbreviated commit
  98.      * id, and short commit message when used.
  99.      *
  100.      * @param message
  101.      *            the working directory message
  102.      * @return {@code this}
  103.      */
  104.     public StashCreateCommand setWorkingDirectoryMessage(String message) {
  105.         workingDirectoryMessage = message;
  106.         return this;
  107.     }

  108.     /**
  109.      * Set the person to use as the author and committer in the commits made
  110.      *
  111.      * @param person
  112.      *            the {@link org.eclipse.jgit.lib.PersonIdent} of the person who
  113.      *            creates the stash.
  114.      * @return {@code this}
  115.      */
  116.     public StashCreateCommand setPerson(PersonIdent person) {
  117.         this.person = person;
  118.         return this;
  119.     }

  120.     /**
  121.      * Set the reference to update with the stashed commit id If null, no
  122.      * reference is updated
  123.      * <p>
  124.      * This value defaults to {@link org.eclipse.jgit.lib.Constants#R_STASH}
  125.      *
  126.      * @param ref
  127.      *            the name of the {@code Ref} to update
  128.      * @return {@code this}
  129.      */
  130.     public StashCreateCommand setRef(String ref) {
  131.         this.ref = ref;
  132.         return this;
  133.     }

  134.     /**
  135.      * Whether to include untracked files in the stash.
  136.      *
  137.      * @param includeUntracked
  138.      *            whether to include untracked files in the stash
  139.      * @return {@code this}
  140.      * @since 3.4
  141.      */
  142.     public StashCreateCommand setIncludeUntracked(boolean includeUntracked) {
  143.         this.includeUntracked = includeUntracked;
  144.         return this;
  145.     }

  146.     private RevCommit parseCommit(final ObjectReader reader,
  147.             final ObjectId headId) throws IOException {
  148.         try (RevWalk walk = new RevWalk(reader)) {
  149.             return walk.parseCommit(headId);
  150.         }
  151.     }

  152.     private CommitBuilder createBuilder() {
  153.         CommitBuilder builder = new CommitBuilder();
  154.         PersonIdent author = person;
  155.         if (author == null)
  156.             author = new PersonIdent(repo);
  157.         builder.setAuthor(author);
  158.         builder.setCommitter(author);
  159.         return builder;
  160.     }

  161.     private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent,
  162.             String refLogMessage) throws IOException {
  163.         if (ref == null)
  164.             return;
  165.         Ref currentRef = repo.findRef(ref);
  166.         RefUpdate refUpdate = repo.updateRef(ref);
  167.         refUpdate.setNewObjectId(commitId);
  168.         refUpdate.setRefLogIdent(refLogIdent);
  169.         refUpdate.setRefLogMessage(refLogMessage, false);
  170.         refUpdate.setForceRefLog(true);
  171.         if (currentRef != null)
  172.             refUpdate.setExpectedOldObjectId(currentRef.getObjectId());
  173.         else
  174.             refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
  175.         refUpdate.forceUpdate();
  176.     }

  177.     private Ref getHead() throws GitAPIException {
  178.         try {
  179.             Ref head = repo.exactRef(Constants.HEAD);
  180.             if (head == null || head.getObjectId() == null)
  181.                 throw new NoHeadException(JGitText.get().headRequiredToStash);
  182.             return head;
  183.         } catch (IOException e) {
  184.             throw new JGitInternalException(JGitText.get().stashFailed, e);
  185.         }
  186.     }

  187.     /**
  188.      * {@inheritDoc}
  189.      * <p>
  190.      * Stash the contents on the working directory and index in separate commits
  191.      * and reset to the current HEAD commit.
  192.      */
  193.     @Override
  194.     public RevCommit call() throws GitAPIException {
  195.         checkCallable();

  196.         List<String> deletedFiles = new ArrayList<>();
  197.         Ref head = getHead();
  198.         try (ObjectReader reader = repo.newObjectReader()) {
  199.             RevCommit headCommit = parseCommit(reader, head.getObjectId());
  200.             DirCache cache = repo.lockDirCache();
  201.             ObjectId commitId;
  202.             try (ObjectInserter inserter = repo.newObjectInserter();
  203.                     TreeWalk treeWalk = new TreeWalk(repo, reader)) {

  204.                 treeWalk.setRecursive(true);
  205.                 treeWalk.addTree(headCommit.getTree());
  206.                 treeWalk.addTree(new DirCacheIterator(cache));
  207.                 treeWalk.addTree(new FileTreeIterator(repo));
  208.                 treeWalk.getTree(2, FileTreeIterator.class)
  209.                         .setDirCacheIterator(treeWalk, 1);
  210.                 treeWalk.setFilter(AndTreeFilter.create(new SkipWorkTreeFilter(
  211.                         1), new IndexDiffFilter(1, 2)));

  212.                 // Return null if no local changes to stash
  213.                 if (!treeWalk.next())
  214.                     return null;

  215.                 MutableObjectId id = new MutableObjectId();
  216.                 List<PathEdit> wtEdits = new ArrayList<>();
  217.                 List<String> wtDeletes = new ArrayList<>();
  218.                 List<DirCacheEntry> untracked = new ArrayList<>();
  219.                 boolean hasChanges = false;
  220.                 do {
  221.                     AbstractTreeIterator headIter = treeWalk.getTree(0,
  222.                             AbstractTreeIterator.class);
  223.                     DirCacheIterator indexIter = treeWalk.getTree(1,
  224.                             DirCacheIterator.class);
  225.                     WorkingTreeIterator wtIter = treeWalk.getTree(2,
  226.                             WorkingTreeIterator.class);
  227.                     if (indexIter != null
  228.                             && !indexIter.getDirCacheEntry().isMerged())
  229.                         throw new UnmergedPathsException(
  230.                                 new UnmergedPathException(
  231.                                         indexIter.getDirCacheEntry()));
  232.                     if (wtIter != null) {
  233.                         if (indexIter == null && headIter == null
  234.                                 && !includeUntracked)
  235.                             continue;
  236.                         hasChanges = true;
  237.                         if (indexIter != null && wtIter.idEqual(indexIter))
  238.                             continue;
  239.                         if (headIter != null && wtIter.idEqual(headIter))
  240.                             continue;
  241.                         treeWalk.getObjectId(id, 0);
  242.                         final DirCacheEntry entry = new DirCacheEntry(
  243.                                 treeWalk.getRawPath());
  244.                         entry.setLength(wtIter.getEntryLength());
  245.                         entry.setLastModified(
  246.                                 wtIter.getEntryLastModifiedInstant());
  247.                         entry.setFileMode(wtIter.getEntryFileMode());
  248.                         long contentLength = wtIter.getEntryContentLength();
  249.                         try (InputStream in = wtIter.openEntryStream()) {
  250.                             entry.setObjectId(inserter.insert(
  251.                                     Constants.OBJ_BLOB, contentLength, in));
  252.                         }

  253.                         if (indexIter == null && headIter == null)
  254.                             untracked.add(entry);
  255.                         else
  256.                             wtEdits.add(new PathEdit(entry) {
  257.                                 @Override
  258.                                 public void apply(DirCacheEntry ent) {
  259.                                     ent.copyMetaData(entry);
  260.                                 }
  261.                             });
  262.                     }
  263.                     hasChanges = true;
  264.                     if (wtIter == null && headIter != null)
  265.                         wtDeletes.add(treeWalk.getPathString());
  266.                 } while (treeWalk.next());

  267.                 if (!hasChanges)
  268.                     return null;

  269.                 String branch = Repository.shortenRefName(head.getTarget()
  270.                         .getName());

  271.                 // Commit index changes
  272.                 CommitBuilder builder = createBuilder();
  273.                 builder.setParentId(headCommit);
  274.                 builder.setTreeId(cache.writeTree(inserter));
  275.                 builder.setMessage(MessageFormat.format(indexMessage, branch,
  276.                         headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH)
  277.                                 .name(),
  278.                         headCommit.getShortMessage()));
  279.                 ObjectId indexCommit = inserter.insert(builder);

  280.                 // Commit untracked changes
  281.                 ObjectId untrackedCommit = null;
  282.                 if (!untracked.isEmpty()) {
  283.                     DirCache untrackedDirCache = DirCache.newInCore();
  284.                     DirCacheBuilder untrackedBuilder = untrackedDirCache
  285.                             .builder();
  286.                     for (DirCacheEntry entry : untracked)
  287.                         untrackedBuilder.add(entry);
  288.                     untrackedBuilder.finish();

  289.                     builder.setParentIds(new ObjectId[0]);
  290.                     builder.setTreeId(untrackedDirCache.writeTree(inserter));
  291.                     builder.setMessage(MessageFormat.format(MSG_UNTRACKED,
  292.                             branch,
  293.                             headCommit
  294.                                     .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH)
  295.                                     .name(),
  296.                             headCommit.getShortMessage()));
  297.                     untrackedCommit = inserter.insert(builder);
  298.                 }

  299.                 // Commit working tree changes
  300.                 if (!wtEdits.isEmpty() || !wtDeletes.isEmpty()) {
  301.                     DirCacheEditor editor = cache.editor();
  302.                     for (PathEdit edit : wtEdits)
  303.                         editor.add(edit);
  304.                     for (String path : wtDeletes)
  305.                         editor.add(new DeletePath(path));
  306.                     editor.finish();
  307.                 }
  308.                 builder.setParentId(headCommit);
  309.                 builder.addParentId(indexCommit);
  310.                 if (untrackedCommit != null)
  311.                     builder.addParentId(untrackedCommit);
  312.                 builder.setMessage(MessageFormat.format(
  313.                         workingDirectoryMessage, branch,
  314.                         headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH)
  315.                                 .name(),
  316.                         headCommit.getShortMessage()));
  317.                 builder.setTreeId(cache.writeTree(inserter));
  318.                 commitId = inserter.insert(builder);
  319.                 inserter.flush();

  320.                 updateStashRef(commitId, builder.getAuthor(),
  321.                         builder.getMessage());

  322.                 // Remove untracked files
  323.                 if (includeUntracked) {
  324.                     for (DirCacheEntry entry : untracked) {
  325.                         String repoRelativePath = entry.getPathString();
  326.                         File file = new File(repo.getWorkTree(),
  327.                                 repoRelativePath);
  328.                         FileUtils.delete(file);
  329.                         deletedFiles.add(repoRelativePath);
  330.                     }
  331.                 }

  332.             } finally {
  333.                 cache.unlock();
  334.             }

  335.             // Hard reset to HEAD
  336.             new ResetCommand(repo).setMode(ResetType.HARD).call();

  337.             // Return stashed commit
  338.             return parseCommit(reader, commitId);
  339.         } catch (IOException e) {
  340.             throw new JGitInternalException(JGitText.get().stashFailed, e);
  341.         } finally {
  342.             if (!deletedFiles.isEmpty()) {
  343.                 repo.fireEvent(
  344.                         new WorkingTreeModifiedEvent(null, deletedFiles));
  345.             }
  346.         }
  347.     }
  348. }