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 java.io.File;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.text.MessageFormat;
  15. import java.util.ArrayList;
  16. import java.util.List;

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

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

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

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

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

  64.     private String indexMessage = MSG_INDEX;

  65.     private String workingDirectoryMessage = MSG_WORKING_DIR;

  66.     private String ref = Constants.R_STASH;

  67.     private PersonIdent person;

  68.     private boolean includeUntracked;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  287.                     builder.setParentIds(new ObjectId[0]);
  288.                     builder.setTreeId(untrackedDirCache.writeTree(inserter));
  289.                     builder.setMessage(MessageFormat.format(MSG_UNTRACKED,
  290.                             branch, headCommit.abbreviate(7).name(),
  291.                             headCommit.getShortMessage()));
  292.                     untrackedCommit = inserter.insert(builder);
  293.                 }

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

  314.                 updateStashRef(commitId, builder.getAuthor(),
  315.                         builder.getMessage());

  316.                 // Remove untracked files
  317.                 if (includeUntracked) {
  318.                     for (DirCacheEntry entry : untracked) {
  319.                         String repoRelativePath = entry.getPathString();
  320.                         File file = new File(repo.getWorkTree(),
  321.                                 repoRelativePath);
  322.                         FileUtils.delete(file);
  323.                         deletedFiles.add(repoRelativePath);
  324.                     }
  325.                 }

  326.             } finally {
  327.                 cache.unlock();
  328.             }

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

  331.             // Return stashed commit
  332.             return parseCommit(reader, commitId);
  333.         } catch (IOException e) {
  334.             throw new JGitInternalException(JGitText.get().stashFailed, e);
  335.         } finally {
  336.             if (!deletedFiles.isEmpty()) {
  337.                 repo.fireEvent(
  338.                         new WorkingTreeModifiedEvent(null, deletedFiles));
  339.             }
  340.         }
  341.     }
  342. }