You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

StashCreateCommand.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. }