Browse Source

Allow to include untracked files in stash operations.

Unstashed changes are saved in a commit which is added as an additional
parent to the stash commit.
This behaviour is fully compatible with C Git stashing of untracked
files.

Bug: 434411
Change-Id: I2af784deb0c2320bb57bc4fd472a8daad8674e7d
Signed-off-by: Andreas Hermann <a.v.hermann@gmail.com>
tags/v3.4.0.201405281120-rc2
Andreas Hermann 10 years ago
parent
commit
44f81d956b

+ 89
- 1
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java View File

@@ -608,7 +608,8 @@ public class StashApplyCommandTest extends RepositoryTestCase {
fail("Exception not thrown");
} catch (JGitInternalException e) {
assertEquals(MessageFormat.format(
JGitText.get().stashCommitMissingTwoParents, head.name()),
JGitText.get().stashCommitIncorrectNumberOfParents,
head.name(), 0),
e.getMessage());
}
}
@@ -648,4 +649,91 @@ public class StashApplyCommandTest extends RepositoryTestCase {

assertFalse(file.exists());
}

@Test
public void untrackedFileNotIncluded() throws Exception {
String untrackedPath = "untracked.txt";
File untrackedFile = writeTrashFile(untrackedPath, "content");
// at least one modification needed
writeTrashFile(PATH, "content2");
git.add().addFilepattern(PATH).call();
git.stashCreate().call();
assertTrue(untrackedFile.exists());

git.stashApply().setStashRef("stash@{0}").call();
assertTrue(untrackedFile.exists());

Status status = git.status().call();
assertEquals(1, status.getUntracked().size());
assertTrue(status.getUntracked().contains(untrackedPath));
assertEquals(1, status.getChanged().size());
assertTrue(status.getChanged().contains(PATH));
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getModified().isEmpty());
}

@Test
public void untrackedFileIncluded() throws Exception {
String path = "a/b/untracked.txt";
File untrackedFile = writeTrashFile(path, "content");
RevCommit stashedCommit = git.stashCreate().setIncludeUntracked(true)
.call();
assertNotNull(stashedCommit);
assertFalse(untrackedFile.exists());
deleteTrashFile("a/b"); // checkout should create parent dirs

git.stashApply().setStashRef("stash@{0}").call();
assertTrue(untrackedFile.exists());
assertEquals("content", read(path));

Status status = git.status().call();
assertEquals(1, status.getUntracked().size());
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getUntracked().contains(path));
}

@Test
public void untrackedFileConflictsWithCommit() throws Exception {
String path = "untracked.txt";
writeTrashFile(path, "untracked");
git.stashCreate().setIncludeUntracked(true).call();

writeTrashFile(path, "committed");
head = git.commit().setMessage("add file").call();
git.add().addFilepattern(path).call();
git.commit().setMessage("conflicting commit").call();

try {
git.stashApply().setStashRef("stash@{0}").call();
fail("StashApplyFailureException should be thrown.");
} catch (StashApplyFailureException e) {
assertEquals(e.getMessage(), JGitText.get().stashApplyConflict);
}
assertEquals("committed", read(path));
}

@Test
public void untrackedFileConflictsWithWorkingDirectory()
throws Exception {
String path = "untracked.txt";
writeTrashFile(path, "untracked");
git.stashCreate().setIncludeUntracked(true).call();

writeTrashFile(path, "working-directory");
try {
git.stashApply().setStashRef("stash@{0}").call();
fail("StashApplyFailureException should be thrown.");
} catch (StashApplyFailureException e) {
assertEquals(e.getMessage(), JGitText.get().stashApplyConflict);
}
assertEquals("working-directory", read(path));
}
}

+ 44
- 3
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java View File

@@ -80,6 +80,8 @@ public class StashCreateCommandTest extends RepositoryTestCase {

private File committedFile;

private File untrackedFile;

@Before
public void setUp() throws Exception {
super.setUp();
@@ -88,16 +90,24 @@ public class StashCreateCommandTest extends RepositoryTestCase {
git.add().addFilepattern("file.txt").call();
head = git.commit().setMessage("add file").call();
assertNotNull(head);
writeTrashFile("untracked.txt", "content");
untrackedFile = writeTrashFile("untracked.txt", "content");
}

private void validateStashedCommit(final RevCommit commit)
throws IOException {
validateStashedCommit(commit, 2);
}

/**
* Core validation to be performed on all stashed commits
*
* @param commit
* @param parentCount
* number of parent commits required
* @throws IOException
*/
private void validateStashedCommit(final RevCommit commit)
private void validateStashedCommit(final RevCommit commit,
int parentCount)
throws IOException {
assertNotNull(commit);
Ref stashRef = db.getRef(Constants.R_STASH);
@@ -105,7 +115,7 @@ public class StashCreateCommandTest extends RepositoryTestCase {
assertEquals(commit, stashRef.getObjectId());
assertNotNull(commit.getAuthorIdent());
assertEquals(commit.getAuthorIdent(), commit.getCommitterIdent());
assertEquals(2, commit.getParentCount());
assertEquals(parentCount, commit.getParentCount());

// Load parents
RevWalk walk = new RevWalk(db);
@@ -461,4 +471,35 @@ public class StashCreateCommandTest extends RepositoryTestCase {

git.stashCreate().call();
}

@Test
public void untrackedFileIncluded() throws Exception {
String trackedPath = "tracked.txt";
writeTrashFile(trackedPath, "content2");
git.add().addFilepattern(trackedPath).call();

RevCommit stashed = git.stashCreate()
.setIncludeUntracked(true).call();
validateStashedCommit(stashed, 3);

assertEquals(
"Expected commits for workingDir,stashedIndex and untrackedFiles.",
3, stashed.getParentCount());
assertFalse("untracked file should be deleted.", untrackedFile.exists());
}

@Test
public void untrackedFileNotIncluded() throws Exception {
String trackedPath = "tracked.txt";
// at least one modification needed
writeTrashFile(trackedPath, "content2");
git.add().addFilepattern(trackedPath).call();

RevCommit stashed = git.stashCreate().call();
validateStashedCommit(stashed);

assertTrue("untracked file should be left untouched.",
untrackedFile.exists());
assertEquals("content", read(untrackedFile));
}
}

+ 1
- 1
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties View File

@@ -476,7 +476,7 @@ stashApplyConflictInIndex=Applying stashed index changes resulted in a conflict.
stashApplyFailed=Applying stashed changes did not successfully complete
stashApplyOnUnsafeRepository=Cannot apply stashed commit on a repository with state: {0}
stashApplyWithoutHead=Cannot apply stashed commit in an empty repository or onto an unborn branch
stashCommitMissingTwoParents=Stashed commit ''{0}'' does not have two parent commits
stashCommitIncorrectNumberOfParents=Stashed commit ''{0}'' does have {1} parent commits instead of 2 or 3.
stashDropDeleteRefFailed=Deleting stash reference failed with result: {0}
stashDropFailed=Dropping stashed commit failed
stashDropMissingReflog=Stash reflog does not contain entry ''{0}''

+ 95
- 3
org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java View File

@@ -42,6 +42,7 @@
*/
package org.eclipse.jgit.api;

import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;

@@ -56,6 +57,7 @@ import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.CheckoutConflictException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -90,6 +92,8 @@ public class StashApplyCommand extends GitCommand<ObjectId> {

private boolean applyIndex = true;

private boolean applyUntracked = true;

private boolean ignoreRepositoryState;

private MergeStrategy strategy = MergeStrategy.RECURSIVE;
@@ -173,15 +177,20 @@ public class StashApplyCommand extends GitCommand<ObjectId> {

final ObjectId stashId = getStashId();
RevCommit stashCommit = revWalk.parseCommit(stashId);
if (stashCommit.getParentCount() != 2)
if (stashCommit.getParentCount() < 2
|| stashCommit.getParentCount() > 3)
throw new JGitInternalException(MessageFormat.format(
JGitText.get().stashCommitMissingTwoParents,
stashId.name()));
JGitText.get().stashCommitIncorrectNumberOfParents,
stashId.name(),
Integer.valueOf(stashCommit.getParentCount())));

ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$
ObjectId stashIndexCommit = revWalk.parseCommit(stashCommit
.getParent(1));
ObjectId stashHeadCommit = stashCommit.getParent(0);
ObjectId untrackedCommit = null;
if (applyUntracked && stashCommit.getParentCount() == 3)
untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2));

ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
merger.setCommitNames(new String[] { "stashed HEAD", "HEAD",
@@ -209,6 +218,29 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
JGitText.get().stashApplyConflict);
}
}

if (untrackedCommit != null) {
ResolveMerger untrackedMerger = (ResolveMerger) strategy
.newMerger(repo, true);
untrackedMerger.setCommitNames(new String[] {
"stashed HEAD", "HEAD", "untracked files" }); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
untrackedMerger.setBase(stashHeadCommit);
boolean ok = untrackedMerger.merge(headCommit,
untrackedCommit);
if (ok)
try {
RevTree untrackedTree = revWalk
.parseTree(untrackedMerger
.getResultTreeId());
resetUntracked(untrackedTree);
} catch (CheckoutConflictException e) {
throw new StashApplyFailureException(
JGitText.get().stashApplyConflict);
}
else
throw new StashApplyFailureException(
JGitText.get().stashApplyConflict);
}
} else {
throw new StashApplyFailureException(
JGitText.get().stashApplyConflict);
@@ -244,6 +276,15 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
return this;
}

/**
* @param applyUntracked
* true (default) if the command should restore untracked files
* @since 3.4
*/
public void setApplyUntracked(boolean applyUntracked) {
this.applyUntracked = applyUntracked;
}

private void resetIndex(RevTree tree) throws IOException {
DirCache dc = repo.lockDirCache();
TreeWalk walk = null;
@@ -285,4 +326,55 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
walk.release();
}
}

private void resetUntracked(RevTree tree) throws CheckoutConflictException,
IOException {
TreeWalk walk = null;
try {
walk = new TreeWalk(repo); // maybe NameConflictTreeWalk?
walk.addTree(tree);
walk.addTree(new FileTreeIterator(repo));
walk.setRecursive(true);

final ObjectReader reader = walk.getObjectReader();

while (walk.next()) {
final AbstractTreeIterator cIter = walk.getTree(0,
AbstractTreeIterator.class);
if (cIter == null)
// Not in commit, don't create untracked
continue;

final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
entry.setFileMode(cIter.getEntryFileMode());
entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());

FileTreeIterator fIter = walk
.getTree(1, FileTreeIterator.class);
if (fIter != null) {
if (fIter.isModified(entry, true, reader)) {
// file exists and is dirty
throw new CheckoutConflictException(
entry.getPathString());
}
}

checkoutPath(entry, reader);
}
} finally {
if (walk != null)
walk.release();
}
}

private void checkoutPath(DirCacheEntry entry, ObjectReader reader) {
try {
File file = new File(repo.getWorkTree(), entry.getPathString());
DirCacheCheckout.checkoutEntry(repo, file, entry, reader);
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().checkoutConflictWithFile,
entry.getPathString()), e);
}
}
}

+ 65
- 9
org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java View File

@@ -42,6 +42,7 @@
*/
package org.eclipse.jgit.api;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
@@ -54,6 +55,7 @@ import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.api.errors.UnmergedPathsException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
@@ -80,6 +82,7 @@ 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;
import org.eclipse.jgit.util.FileUtils;

/**
* Command class to stash changes in the working directory and index in a
@@ -93,6 +96,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> {

private static final String MSG_INDEX = "index on {0}: {1} {2}";

private static final String MSG_UNTRACKED = "untracked files on {0}: {1} {2}";

private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}";

private String indexMessage = MSG_INDEX;
@@ -103,6 +108,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> {

private PersonIdent person;

private boolean includeUntracked;

/**
* Create a command to stash changes in the working directory and index
*
@@ -166,6 +173,18 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
return this;
}

/**
* Whether to include untracked files in the stash.
*
* @param includeUntracked
* @return {@code this}
* @since 3.4
*/
public StashCreateCommand setIncludeUntracked(boolean includeUntracked) {
this.includeUntracked = includeUntracked;
return this;
}

private RevCommit parseCommit(final ObjectReader reader,
final ObjectId headId) throws IOException {
final RevWalk walk = new RevWalk(reader);
@@ -173,14 +192,13 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
return walk.parseCommit(headId);
}

private CommitBuilder createBuilder(ObjectId headId) {
private CommitBuilder createBuilder() {
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;
}

@@ -244,6 +262,7 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
MutableObjectId id = new MutableObjectId();
List<PathEdit> wtEdits = new ArrayList<PathEdit>();
List<String> wtDeletes = new ArrayList<String>();
List<DirCacheEntry> untracked = new ArrayList<DirCacheEntry>();
boolean hasChanges = false;
do {
AbstractTreeIterator headIter = treeWalk.getTree(0,
@@ -258,7 +277,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
new UnmergedPathException(
indexIter.getDirCacheEntry()));
if (wtIter != null) {
if (indexIter == null && headIter == null)
if (indexIter == null && headIter == null
&& !includeUntracked)
continue;
hasChanges = true;
if (indexIter != null && wtIter.idEqual(indexIter))
@@ -279,11 +299,15 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
} finally {
in.close();
}
wtEdits.add(new PathEdit(entry) {
public void apply(DirCacheEntry ent) {
ent.copyMetaData(entry);
}
});

if (indexIter == null && headIter == null)
untracked.add(entry);
else
wtEdits.add(new PathEdit(entry) {
public void apply(DirCacheEntry ent) {
ent.copyMetaData(entry);
}
});
}
hasChanges = true;
if (wtIter == null && headIter != null)
@@ -297,13 +321,32 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
.getName());

// Commit index changes
CommitBuilder builder = createBuilder(headCommit);
CommitBuilder builder = createBuilder();
builder.setParentId(headCommit);
builder.setTreeId(cache.writeTree(inserter));
builder.setMessage(MessageFormat.format(indexMessage, branch,
headCommit.abbreviate(7).name(),
headCommit.getShortMessage()));
ObjectId indexCommit = inserter.insert(builder);

// Commit untracked changes
ObjectId untrackedCommit = null;
if (!untracked.isEmpty()) {
DirCache untrackedDirCache = DirCache.newInCore();
DirCacheBuilder untrackedBuilder = untrackedDirCache
.builder();
for (DirCacheEntry entry : untracked)
untrackedBuilder.add(entry);
untrackedBuilder.finish();

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

// Commit working tree changes
if (!wtEdits.isEmpty() || !wtDeletes.isEmpty()) {
DirCacheEditor editor = cache.editor();
@@ -313,7 +356,10 @@ public class StashCreateCommand extends GitCommand<RevCommit> {
editor.add(new DeletePath(path));
editor.finish();
}
builder.setParentId(headCommit);
builder.addParentId(indexCommit);
if (untrackedCommit != null)
builder.addParentId(untrackedCommit);
builder.setMessage(MessageFormat.format(
workingDirectoryMessage, branch,
headCommit.abbreviate(7).name(),
@@ -324,6 +370,16 @@ public class StashCreateCommand extends GitCommand<RevCommit> {

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

// Remove untracked files
if (includeUntracked) {
for (DirCacheEntry entry : untracked) {
File file = new File(repo.getWorkTree(),
entry.getPathString());
FileUtils.delete(file);
}
}

} finally {
inserter.release();
cache.unlock();

+ 1
- 1
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java View File

@@ -538,7 +538,7 @@ public class JGitText extends TranslationBundle {
/***/ public String stashApplyFailed;
/***/ public String stashApplyWithoutHead;
/***/ public String stashApplyOnUnsafeRepository;
/***/ public String stashCommitMissingTwoParents;
/***/ public String stashCommitIncorrectNumberOfParents;
/***/ public String stashDropDeleteRefFailed;
/***/ public String stashDropFailed;
/***/ public String stashDropMissingReflog;

Loading…
Cancel
Save