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>
This commit is contained in:
parent
10c0b34b88
commit
03d4dc597e
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -205,6 +205,7 @@ flagIsDisposed={0} is disposed.
|
||||||
flagNotFromThis={0} not from this.
|
flagNotFromThis={0} not from this.
|
||||||
flagsAlreadyCreated={0} flags already created.
|
flagsAlreadyCreated={0} flags already created.
|
||||||
funnyRefname=funny refname
|
funnyRefname=funny refname
|
||||||
|
headRequiredToStash=HEAD required to stash local changes
|
||||||
hoursAgo={0} hours ago
|
hoursAgo={0} hours ago
|
||||||
hugeIndexesAreNotSupportedByJgitYet=Huge indexes are not supported by jgit, yet
|
hugeIndexesAreNotSupportedByJgitYet=Huge indexes are not supported by jgit, yet
|
||||||
hunkBelongsToAnotherFile=Hunk belongs to another file
|
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}
|
sourceRefNotSpecifiedForRefspec=Source ref not specified for refspec: {0}
|
||||||
staleRevFlagsOn=Stale RevFlags on {0}
|
staleRevFlagsOn=Stale RevFlags on {0}
|
||||||
startingReadStageWithoutWrittenRequestDataPendingIsNotSupported=Starting read stage without written request data pending is not supported
|
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
|
statelessRPCRequiresOptionToBeEnabled=stateless RPC requires {0} to be enabled
|
||||||
submoduleExists=Submodule ''{0}'' already exists in the index
|
submoduleExists=Submodule ''{0}'' already exists in the index
|
||||||
submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}''
|
submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}''
|
||||||
|
|
|
@ -265,6 +265,7 @@ public static JGitText get() {
|
||||||
/***/ public String flagNotFromThis;
|
/***/ public String flagNotFromThis;
|
||||||
/***/ public String flagsAlreadyCreated;
|
/***/ public String flagsAlreadyCreated;
|
||||||
/***/ public String funnyRefname;
|
/***/ public String funnyRefname;
|
||||||
|
/***/ public String headRequiredToStash;
|
||||||
/***/ public String hoursAgo;
|
/***/ public String hoursAgo;
|
||||||
/***/ public String hugeIndexesAreNotSupportedByJgitYet;
|
/***/ public String hugeIndexesAreNotSupportedByJgitYet;
|
||||||
/***/ public String hunkBelongsToAnotherFile;
|
/***/ public String hunkBelongsToAnotherFile;
|
||||||
|
@ -482,6 +483,7 @@ public static JGitText get() {
|
||||||
/***/ public String sourceRefNotSpecifiedForRefspec;
|
/***/ public String sourceRefNotSpecifiedForRefspec;
|
||||||
/***/ public String staleRevFlagsOn;
|
/***/ public String staleRevFlagsOn;
|
||||||
/***/ public String startingReadStageWithoutWrittenRequestDataPendingIsNotSupported;
|
/***/ public String startingReadStageWithoutWrittenRequestDataPendingIsNotSupported;
|
||||||
|
/***/ public String stashFailed;
|
||||||
/***/ public String statelessRPCRequiresOptionToBeEnabled;
|
/***/ public String statelessRPCRequiresOptionToBeEnabled;
|
||||||
/***/ public String submoduleExists;
|
/***/ public String submoduleExists;
|
||||||
/***/ public String submodulesNotSupported;
|
/***/ public String submodulesNotSupported;
|
||||||
|
|
|
@ -573,6 +573,15 @@ public StashListCommand stashList() {
|
||||||
return new StashListCommand(repo);
|
return new StashListCommand(repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* @return the git repository this class is interacting with
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue