aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKevin Sawicki <kevin@github.com>2012-02-27 12:44:34 -0800
committerChris Aniszczyk <zx@twitter.com>2012-02-28 13:46:35 -0800
commit03d4dc597e0577575f56f094c27531d62b10316a (patch)
tree5d98f4299af7270f0f74bc0ed5fb2143d9f66512
parent10c0b34b88fe351405c45c19916b0bd627179c7d (diff)
downloadjgit-03d4dc597e0577575f56f094c27531d62b10316a.tar.gz
jgit-03d4dc597e0577575f56f094c27531d62b10316a.zip
Add support for creating a stashed commit
Adds a new command to stash the index and working directory changes in a commit stored in refs/stash Bug: 309355 Change-Id: I2ce85b1601b74b07e286a3f99feb358dfbdfe29c Signed-off-by: Chris Aniszczyk <zx@twitter.com>
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java397
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java316
5 files changed, 726 insertions, 0 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
new file mode 100644
index 0000000000..b91a50a945
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2012, GitHub Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests of {@link StashCreateCommand}
+ */
+public class StashCreateCommandTest extends RepositoryTestCase {
+
+ private RevCommit head;
+
+ private Git git;
+
+ private File committedFile;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ git = Git.wrap(db);
+ committedFile = writeTrashFile("file.txt", "content");
+ git.add().addFilepattern("file.txt").call();
+ head = git.commit().setMessage("add file").call();
+ assertNotNull(head);
+ }
+
+ /**
+ * Core validation to be performed on all stashed commits
+ *
+ * @param commit
+ * @throws IOException
+ */
+ private void validateStashedCommit(final RevCommit commit)
+ throws IOException {
+ assertNotNull(commit);
+ Ref stashRef = db.getRef(Constants.R_STASH);
+ assertNotNull(stashRef);
+ assertEquals(commit, stashRef.getObjectId());
+ assertNotNull(commit.getAuthorIdent());
+ assertEquals(commit.getAuthorIdent(), commit.getCommitterIdent());
+ assertEquals(2, commit.getParentCount());
+
+ // Load parents
+ RevWalk walk = new RevWalk(db);
+ try {
+ for (RevCommit parent : commit.getParents())
+ walk.parseBody(parent);
+ } finally {
+ walk.release();
+ }
+
+ assertEquals(1, commit.getParent(1).getParentCount());
+ assertEquals(head, commit.getParent(1).getParent(0));
+ assertFalse("Head tree matches stashed commit tree", commit.getTree()
+ .equals(head.getTree()));
+ assertEquals(head, commit.getParent(0));
+ assertFalse(commit.getFullMessage().equals(
+ commit.getParent(1).getFullMessage()));
+ }
+
+ private TreeWalk createTreeWalk() {
+ TreeWalk walk = new TreeWalk(db);
+ walk.setRecursive(true);
+ walk.setFilter(TreeFilter.ANY_DIFF);
+ return walk;
+ }
+
+ private List<DiffEntry> diffWorkingAgainstHead(final RevCommit commit)
+ throws IOException {
+ TreeWalk walk = createTreeWalk();
+ try {
+ walk.addTree(commit.getParent(0).getTree());
+ walk.addTree(commit.getTree());
+ return DiffEntry.scan(walk);
+ } finally {
+ walk.release();
+ }
+ }
+
+ private List<DiffEntry> diffIndexAgainstHead(final RevCommit commit)
+ throws IOException {
+ TreeWalk walk = createTreeWalk();
+ try {
+ walk.addTree(commit.getParent(0).getTree());
+ walk.addTree(commit.getParent(1).getTree());
+ return DiffEntry.scan(walk);
+ } finally {
+ walk.release();
+ }
+ }
+
+ @Test
+ public void noLocalChanges() throws Exception {
+ assertNull(git.stashCreate().call());
+ }
+
+ @Test
+ public void workingDirectoryDelete() throws Exception {
+ deleteTrashFile("file.txt");
+ RevCommit stashed = git.stashCreate().call();
+ assertNotNull(stashed);
+ assertEquals("content", read(committedFile));
+ validateStashedCommit(stashed);
+
+ assertEquals(head.getTree(), stashed.getParent(1).getTree());
+
+ List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, diffs.size());
+ assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
+ assertEquals("file.txt", diffs.get(0).getOldPath());
+ }
+
+ @Test
+ public void indexAdd() throws Exception {
+ File addedFile = writeTrashFile("file2.txt", "content2");
+ git.add().addFilepattern("file2.txt").call();
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertFalse(addedFile.exists());
+ validateStashedCommit(stashed);
+
+ assertEquals(stashed.getTree(), stashed.getParent(1).getTree());
+
+ List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, diffs.size());
+ assertEquals(DiffEntry.ChangeType.ADD, diffs.get(0).getChangeType());
+ assertEquals("file2.txt", diffs.get(0).getNewPath());
+ }
+
+ @Test
+ public void indexDelete() throws Exception {
+ git.rm().addFilepattern("file.txt").call();
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertEquals("content", read(committedFile));
+ validateStashedCommit(stashed);
+
+ assertEquals(stashed.getTree(), stashed.getParent(1).getTree());
+
+ List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, diffs.size());
+ assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
+ assertEquals("file.txt", diffs.get(0).getOldPath());
+ }
+
+ @Test
+ public void workingDirectoryModify() throws Exception {
+ writeTrashFile("file.txt", "content2");
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertEquals("content", read(committedFile));
+ validateStashedCommit(stashed);
+
+ assertEquals(head.getTree(), stashed.getParent(1).getTree());
+
+ List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, diffs.size());
+ assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType());
+ assertEquals("file.txt", diffs.get(0).getNewPath());
+ }
+
+ @Test
+ public void workingDirectoryModifyInSubfolder() throws Exception {
+ String path = "d1/d2/f.txt";
+ File subfolderFile = writeTrashFile(path, "content");
+ git.add().addFilepattern(path).call();
+ head = git.commit().setMessage("add file").call();
+
+ writeTrashFile(path, "content2");
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertEquals("content", read(subfolderFile));
+ validateStashedCommit(stashed);
+
+ assertEquals(head.getTree(), stashed.getParent(1).getTree());
+
+ List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, diffs.size());
+ assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType());
+ assertEquals(path, diffs.get(0).getNewPath());
+ }
+
+ @Test
+ public void workingDirectoryModifyIndexChanged() throws Exception {
+ writeTrashFile("file.txt", "content2");
+ git.add().addFilepattern("file.txt").call();
+ writeTrashFile("file.txt", "content3");
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertEquals("content", read(committedFile));
+ validateStashedCommit(stashed);
+
+ assertFalse(stashed.getTree().equals(stashed.getParent(1).getTree()));
+
+ List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, workingDiffs.size());
+ assertEquals(DiffEntry.ChangeType.MODIFY, workingDiffs.get(0)
+ .getChangeType());
+ assertEquals("file.txt", workingDiffs.get(0).getNewPath());
+
+ List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
+ assertEquals(1, indexDiffs.size());
+ assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
+ .getChangeType());
+ assertEquals("file.txt", indexDiffs.get(0).getNewPath());
+
+ assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
+ .getOldId());
+ assertFalse(workingDiffs.get(0).getNewId()
+ .equals(indexDiffs.get(0).getNewId()));
+ }
+
+ @Test
+ public void workingDirectoryCleanIndexModify() throws Exception {
+ writeTrashFile("file.txt", "content2");
+ git.add().addFilepattern("file.txt").call();
+ writeTrashFile("file.txt", "content");
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertEquals("content", read(committedFile));
+ validateStashedCommit(stashed);
+
+ assertTrue(stashed.getTree().equals(stashed.getParent(1).getTree()));
+
+ List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, workingDiffs.size());
+ assertEquals(DiffEntry.ChangeType.MODIFY, workingDiffs.get(0)
+ .getChangeType());
+ assertEquals("file.txt", workingDiffs.get(0).getNewPath());
+
+ List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
+ assertEquals(1, indexDiffs.size());
+ assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
+ .getChangeType());
+ assertEquals("file.txt", indexDiffs.get(0).getNewPath());
+
+ assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
+ .getOldId());
+ assertTrue(workingDiffs.get(0).getNewId()
+ .equals(indexDiffs.get(0).getNewId()));
+ }
+
+ @Test
+ public void workingDirectoryDeleteIndexAdd() throws Exception {
+ String path = "file2.txt";
+ File added = writeTrashFile(path, "content2");
+ assertTrue(added.exists());
+ git.add().addFilepattern(path).call();
+ FileUtils.delete(added);
+ assertFalse(added.exists());
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertFalse(added.exists());
+
+ validateStashedCommit(stashed);
+
+ assertTrue(stashed.getTree().equals(stashed.getParent(1).getTree()));
+
+ List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, workingDiffs.size());
+ assertEquals(DiffEntry.ChangeType.ADD, workingDiffs.get(0)
+ .getChangeType());
+ assertEquals(path, workingDiffs.get(0).getNewPath());
+
+ List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
+ assertEquals(1, indexDiffs.size());
+ assertEquals(DiffEntry.ChangeType.ADD, indexDiffs.get(0)
+ .getChangeType());
+ assertEquals(path, indexDiffs.get(0).getNewPath());
+
+ assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
+ .getOldId());
+ assertTrue(workingDiffs.get(0).getNewId()
+ .equals(indexDiffs.get(0).getNewId()));
+ }
+
+ @Test
+ public void workingDirectoryDeleteIndexEdit() throws Exception {
+ File edited = writeTrashFile("file.txt", "content2");
+ git.add().addFilepattern("file.txt").call();
+ FileUtils.delete(edited);
+ assertFalse(edited.exists());
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertEquals("content", read(committedFile));
+ validateStashedCommit(stashed);
+
+ assertFalse(stashed.getTree().equals(stashed.getParent(1).getTree()));
+
+ List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
+ assertEquals(1, workingDiffs.size());
+ assertEquals(DiffEntry.ChangeType.DELETE, workingDiffs.get(0)
+ .getChangeType());
+ assertEquals("file.txt", workingDiffs.get(0).getOldPath());
+
+ List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
+ assertEquals(1, indexDiffs.size());
+ assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
+ .getChangeType());
+ assertEquals("file.txt", indexDiffs.get(0).getNewPath());
+
+ assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
+ .getOldId());
+ assertFalse(workingDiffs.get(0).getNewId()
+ .equals(indexDiffs.get(0).getNewId()));
+ }
+
+ @Test
+ public void multipleEdits() throws Exception {
+ git.rm().addFilepattern("file.txt").call();
+ File addedFile = writeTrashFile("file2.txt", "content2");
+ git.add().addFilepattern("file2.txt").call();
+
+ RevCommit stashed = Git.wrap(db).stashCreate().call();
+ assertNotNull(stashed);
+ assertFalse(addedFile.exists());
+ validateStashedCommit(stashed);
+
+ assertEquals(stashed.getTree(), stashed.getParent(1).getTree());
+
+ List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
+ assertEquals(2, diffs.size());
+ assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
+ assertEquals("file.txt", diffs.get(0).getOldPath());
+ assertEquals(DiffEntry.ChangeType.ADD, diffs.get(1).getChangeType());
+ assertEquals("file2.txt", diffs.get(1).getNewPath());
+ }
+}
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
index 8a5e023104..5a916f5a43 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
@@ -205,6 +205,7 @@ flagIsDisposed={0} is disposed.
flagNotFromThis={0} not from this.
flagsAlreadyCreated={0} flags already created.
funnyRefname=funny refname
+headRequiredToStash=HEAD required to stash local changes
hoursAgo={0} hours ago
hugeIndexesAreNotSupportedByJgitYet=Huge indexes are not supported by jgit, yet
hunkBelongsToAnotherFile=Hunk belongs to another file
@@ -422,6 +423,7 @@ sourceRefDoesntResolveToAnyObject=Source ref {0} doesn't resolve to any object.
sourceRefNotSpecifiedForRefspec=Source ref not specified for refspec: {0}
staleRevFlagsOn=Stale RevFlags on {0}
startingReadStageWithoutWrittenRequestDataPendingIsNotSupported=Starting read stage without written request data pending is not supported
+stashFailed=Stashing local changes did not successfully complete
statelessRPCRequiresOptionToBeEnabled=stateless RPC requires {0} to be enabled
submoduleExists=Submodule ''{0}'' already exists in the index
submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}''
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
index bcd14c6a4f..0276f68612 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
@@ -265,6 +265,7 @@ public class JGitText extends TranslationBundle {
/***/ public String flagNotFromThis;
/***/ public String flagsAlreadyCreated;
/***/ public String funnyRefname;
+ /***/ public String headRequiredToStash;
/***/ public String hoursAgo;
/***/ public String hugeIndexesAreNotSupportedByJgitYet;
/***/ public String hunkBelongsToAnotherFile;
@@ -482,6 +483,7 @@ public class JGitText extends TranslationBundle {
/***/ public String sourceRefNotSpecifiedForRefspec;
/***/ public String staleRevFlagsOn;
/***/ public String startingReadStageWithoutWrittenRequestDataPendingIsNotSupported;
+ /***/ public String stashFailed;
/***/ public String statelessRPCRequiresOptionToBeEnabled;
/***/ public String submoduleExists;
/***/ public String submodulesNotSupported;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
index 7d34902e93..0c5b56a6cc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -574,6 +574,15 @@ public class Git {
}
/**
+ * Returns a command object used to create a stashed commit
+ *
+ * @return a {@link StashCreateCommand}
+ */
+ public StashCreateCommand stashCreate() {
+ return new StashCreateCommand(repo);
+ }
+
+ /**
* @return the git repository this class is interacting with
*/
public Repository getRepository() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
new file mode 100644
index 0000000000..8ead2b57fa
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2012, GitHub Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.JGitText;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.NoHeadException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
+import org.eclipse.jgit.treewalk.filter.SkipWorkTreeFilter;
+
+/**
+ * Command class to stash changes in the working directory and index in a
+ * commit.
+ *
+ * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
+ * >Git documentation about Stash</a>
+ */
+public class StashCreateCommand extends GitCommand<RevCommit> {
+
+ private static final String MSG_INDEX = "index on {0}: {1} {2}";
+
+ private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}";
+
+ private String indexMessage = MSG_INDEX;
+
+ private String workingDirectoryMessage = MSG_WORKING_DIR;
+
+ private String ref = Constants.R_STASH;
+
+ private PersonIdent person;
+
+ /**
+ * Create a command to stash changes in the working directory and index
+ *
+ * @param repo
+ */
+ public StashCreateCommand(Repository repo) {
+ super(repo);
+ person = new PersonIdent(repo);
+ }
+
+ /**
+ * Set the message used when committing index changes
+ * <p>
+ * The message will be formatted with the current branch, abbreviated commit
+ * id, and short commit message when used.
+ *
+ * @param message
+ * @return {@code this}
+ */
+ public StashCreateCommand setIndexMessage(String message) {
+ indexMessage = message;
+ return this;
+ }
+
+ /**
+ * Set the message used when committing working directory changes
+ * <p>
+ * The message will be formatted with the current branch, abbreviated commit
+ * id, and short commit message when used.
+ *
+ * @param message
+ * @return {@code this}
+ */
+ public StashCreateCommand setWorkingDirectoryMessage(String message) {
+ workingDirectoryMessage = message;
+ return this;
+ }
+
+ /**
+ * Set the person to use as the author and committer in the commits made
+ *
+ * @param person
+ */
+ public void setPerson(PersonIdent person) {
+ this.person = person;
+ }
+
+ /**
+ * Set the reference to update with the stashed commit id
+ * <p>
+ * This value defaults to {@link Constants#R_STASH}
+ *
+ * @param ref
+ */
+ public void setRef(String ref) {
+ this.ref = ref;
+ }
+
+ private RevCommit parseCommit(final ObjectReader reader,
+ final ObjectId headId) throws IOException {
+ final RevWalk walk = new RevWalk(reader);
+ walk.setRetainBody(true);
+ return walk.parseCommit(headId);
+ }
+
+ private CommitBuilder createBuilder(ObjectId headId) {
+ CommitBuilder builder = new CommitBuilder();
+ PersonIdent author = person;
+ if (author == null)
+ author = new PersonIdent(repo);
+ builder.setAuthor(author);
+ builder.setCommitter(author);
+ builder.setParentId(headId);
+ return builder;
+ }
+
+ private void updateStashRef(ObjectId commitId) throws IOException {
+ Ref currentRef = repo.getRef(ref);
+ RefUpdate refUpdate = repo.updateRef(ref);
+ refUpdate.setNewObjectId(commitId);
+ if (currentRef != null)
+ refUpdate.setExpectedOldObjectId(currentRef.getObjectId());
+ else
+ refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+ refUpdate.forceUpdate();
+ }
+
+ private Ref getHead() throws GitAPIException {
+ try {
+ Ref head = repo.getRef(Constants.HEAD);
+ if (head == null || head.getObjectId() == null)
+ throw new NoHeadException(JGitText.get().headRequiredToStash);
+ return head;
+ } catch (IOException e) {
+ throw new JGitInternalException(JGitText.get().stashFailed, e);
+ }
+ }
+
+ /**
+ * Stash the contents on the working directory and index in separate commits
+ * and reset to the current HEAD commit.
+ *
+ * @return stashed commit or null if no changes to stash
+ */
+ public RevCommit call() throws GitAPIException, JGitInternalException {
+ checkCallable();
+
+ Ref head = getHead();
+ ObjectReader reader = repo.newObjectReader();
+ try {
+ RevCommit headCommit = parseCommit(reader, head.getObjectId());
+ DirCache cache = repo.lockDirCache();
+ ObjectInserter inserter = repo.newObjectInserter();
+ ObjectId commitId;
+ try {
+ TreeWalk treeWalk = new TreeWalk(reader);
+ treeWalk.setRecursive(true);
+ treeWalk.addTree(headCommit.getTree());
+ treeWalk.addTree(new DirCacheIterator(cache));
+ treeWalk.addTree(new FileTreeIterator(repo));
+ treeWalk.setFilter(AndTreeFilter.create(new SkipWorkTreeFilter(
+ 1), new IndexDiffFilter(1, 2)));
+
+ // Return null if no local changes to stash
+ if (!treeWalk.next())
+ return null;
+
+ MutableObjectId id = new MutableObjectId();
+ List<PathEdit> wtEdits = new ArrayList<PathEdit>();
+ List<String> wtDeletes = new ArrayList<String>();
+ do {
+ AbstractTreeIterator headIter = treeWalk.getTree(0,
+ AbstractTreeIterator.class);
+ DirCacheIterator indexIter = treeWalk.getTree(1,
+ DirCacheIterator.class);
+ WorkingTreeIterator wtIter = treeWalk.getTree(2,
+ WorkingTreeIterator.class);
+ if (headIter != null && indexIter != null && wtIter != null) {
+ if (wtIter.idEqual(indexIter)
+ || wtIter.idEqual(headIter))
+ continue;
+ treeWalk.getObjectId(id, 0);
+ final DirCacheEntry entry = new DirCacheEntry(
+ treeWalk.getRawPath());
+ entry.setLength(wtIter.getEntryLength());
+ entry.setLastModified(wtIter.getEntryLastModified());
+ entry.setFileMode(wtIter.getEntryFileMode());
+ InputStream in = wtIter.openEntryStream();
+ try {
+ entry.setObjectId(inserter.insert(
+ Constants.OBJ_BLOB,
+ wtIter.getEntryLength(), in));
+ } finally {
+ in.close();
+ }
+ wtEdits.add(new PathEdit(entry) {
+
+ public void apply(DirCacheEntry ent) {
+ ent.copyMetaData(entry);
+ }
+ });
+ } else if (indexIter == null)
+ wtDeletes.add(treeWalk.getPathString());
+ else if (wtIter == null && headIter != null)
+ wtDeletes.add(treeWalk.getPathString());
+ } while (treeWalk.next());
+
+ String branch = Repository.shortenRefName(head.getTarget()
+ .getName());
+
+ // Commit index changes
+ CommitBuilder builder = createBuilder(headCommit);
+ builder.setTreeId(cache.writeTree(inserter));
+ builder.setMessage(MessageFormat.format(indexMessage, branch,
+ headCommit.abbreviate(7).name(),
+ headCommit.getShortMessage()));
+ ObjectId indexCommit = inserter.insert(builder);
+
+ // Commit working tree changes
+ if (!wtEdits.isEmpty() || !wtDeletes.isEmpty()) {
+ DirCacheEditor editor = cache.editor();
+ for (PathEdit edit : wtEdits)
+ editor.add(edit);
+ for (String path : wtDeletes)
+ editor.add(new DeletePath(path));
+ editor.finish();
+ }
+ builder.addParentId(indexCommit);
+ builder.setMessage(MessageFormat.format(
+ workingDirectoryMessage, branch,
+ headCommit.abbreviate(7).name(),
+ headCommit.getShortMessage()));
+ builder.setTreeId(cache.writeTree(inserter));
+ commitId = inserter.insert(builder);
+ inserter.flush();
+
+ updateStashRef(commitId);
+ } finally {
+ inserter.release();
+ cache.unlock();
+ }
+
+ // Hard reset to HEAD
+ new ResetCommand(repo).setMode(ResetType.HARD).call();
+
+ // Return stashed commit
+ return parseCommit(reader, commitId);
+ } catch (IOException e) {
+ throw new JGitInternalException(JGitText.get().stashFailed, e);
+ } finally {
+ reader.release();
+ }
+ }
+}