Browse Source

Delete non empty directories before checkout a path

If the checkout path is currently a non-empty directory (and was a link
or a regular file before), this directory will be removed before
performing checkout, but only if the checkout path is specified.

Bug: 474973
Change-Id: Ifc6c61592d9b54d26c66367163acdebea369145c
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
stable-4.2
Andrey Loskutov 9 years ago
parent
commit
a406ebf401
  1. 7
      org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java
  2. 6
      org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
  3. 392
      org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
  4. 8
      org.eclipse.jgit/.settings/.api_filters
  5. 2
      org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
  6. 2
      org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
  7. 55
      org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
  8. 13
      org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java

7
org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java

@ -55,6 +55,7 @@ import java.io.Writer;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.FileUtils;
@ -240,4 +241,10 @@ public abstract class JGitTestUtil {
FileUtils.delete(path);
}
public static Path writeLink(Repository db, String link,
String target) throws Exception {
return FileUtils.createSymLink(new File(db.getWorkTree(), link),
target);
}
}

6
org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java

@ -55,6 +55,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Path;
import java.util.Map;
import org.eclipse.jgit.api.Git;
@ -107,6 +108,11 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase {
return JGitTestUtil.writeTrashFile(db, name, data);
}
protected Path writeLink(final String link, final String target)
throws Exception {
return JGitTestUtil.writeLink(db, link, target);
}
protected File writeTrashFile(final String subdir, final String name,
final String data)
throws IOException {

392
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java

@ -923,6 +923,299 @@ public class DirCacheCheckoutTest extends RepositoryTestCase {
}
}
@Test
public void testCheckoutChangeLinkToEmptyDir() throws Exception {
String fname = "was_file";
Git git = Git.wrap(db);
// Add a file
writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
// Add a link to file
String linkName = "link";
File link = writeLink(linkName, fname).toFile();
git.add().addFilepattern(linkName).call();
git.commit().setMessage("Added file and link").call();
assertWorkDir(mkmap(linkName, "a", fname, "a"));
// replace link with empty directory
FileUtils.delete(link);
FileUtils.mkdir(link);
assertTrue("Link must be a directory now", link.isDirectory());
// modify file
writeTrashFile(fname, "b");
assertWorkDir(mkmap(fname, "b", linkName, "/"));
// revert both paths to HEAD state
git.checkout().setStartPoint(Constants.HEAD)
.addPath(fname).addPath(linkName).call();
assertWorkDir(mkmap(fname, "a", linkName, "a"));
Status st = git.status().call();
assertTrue(st.isClean());
}
@Test
public void testCheckoutChangeLinkToEmptyDirs() throws Exception {
String fname = "was_file";
Git git = Git.wrap(db);
// Add a file
writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
// Add a link to file
String linkName = "link";
File link = writeLink(linkName, fname).toFile();
git.add().addFilepattern(linkName).call();
git.commit().setMessage("Added file and link").call();
assertWorkDir(mkmap(linkName, "a", fname, "a"));
// replace link with directory containing only directories, no files
FileUtils.delete(link);
FileUtils.mkdirs(new File(link, "dummyDir"));
assertTrue("Link must be a directory now", link.isDirectory());
assertFalse("Must not delete non empty directory", link.delete());
// modify file
writeTrashFile(fname, "b");
assertWorkDir(mkmap(fname, "b", linkName + "/dummyDir", "/"));
// revert both paths to HEAD state
git.checkout().setStartPoint(Constants.HEAD)
.addPath(fname).addPath(linkName).call();
assertWorkDir(mkmap(fname, "a", linkName, "a"));
Status st = git.status().call();
assertTrue(st.isClean());
}
@Test
public void testCheckoutChangeLinkToNonEmptyDirs() throws Exception {
String fname = "file";
Git git = Git.wrap(db);
// Add a file
writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
// Add a link to file
String linkName = "link";
File link = writeLink(linkName, fname).toFile();
git.add().addFilepattern(linkName).call();
git.commit().setMessage("Added file and link").call();
assertWorkDir(mkmap(linkName, "a", fname, "a"));
// replace link with directory containing only directories, no files
FileUtils.delete(link);
// create but do not add a file in the new directory to the index
writeTrashFile(linkName + "/dir1", "file1", "c");
// create but do not add a file in the new directory to the index
writeTrashFile(linkName + "/dir2", "file2", "d");
assertTrue("File must be a directory now", link.isDirectory());
assertFalse("Must not delete non empty directory", link.delete());
// 2 extra files are created
assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c",
linkName + "/dir2/file2", "d"));
// revert path to HEAD state
git.checkout().setStartPoint(Constants.HEAD).addPath(linkName).call();
// expect only the one added to the index
assertWorkDir(mkmap(linkName, "a", fname, "a"));
Status st = git.status().call();
assertTrue(st.isClean());
}
@Test
public void testCheckoutChangeLinkToNonEmptyDirsAndNewIndexEntry()
throws Exception {
String fname = "file";
Git git = Git.wrap(db);
// Add a file
writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
// Add a link to file
String linkName = "link";
File link = writeLink(linkName, fname).toFile();
git.add().addFilepattern(linkName).call();
git.commit().setMessage("Added file and link").call();
assertWorkDir(mkmap(linkName, "a", fname, "a"));
// replace link with directory containing only directories, no files
FileUtils.delete(link);
// create and add a file in the new directory to the index
writeTrashFile(linkName + "/dir1", "file1", "c");
git.add().addFilepattern(linkName + "/dir1/file1").call();
// create but do not add a file in the new directory to the index
writeTrashFile(linkName + "/dir2", "file2", "d");
assertTrue("File must be a directory now", link.isDirectory());
assertFalse("Must not delete non empty directory", link.delete());
// 2 extra files are created
assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c",
linkName + "/dir2/file2", "d"));
// revert path to HEAD state
git.checkout().setStartPoint(Constants.HEAD).addPath(linkName).call();
// original file and link
assertWorkDir(mkmap(linkName, "a", fname, "a"));
Status st = git.status().call();
assertFalse(st.isClean());
}
@Test
public void testCheckoutChangeFileToEmptyDir() throws Exception {
String fname = "was_file";
Git git = Git.wrap(db);
// Add a file
File file = writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
git.commit().setMessage("Added file").call();
// replace file with empty directory
FileUtils.delete(file);
FileUtils.mkdir(file);
assertTrue("File must be a directory now", file.isDirectory());
assertWorkDir(mkmap(fname, "/"));
// revert path to HEAD state
git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
assertWorkDir(mkmap(fname, "a"));
Status st = git.status().call();
assertTrue(st.isClean());
}
@Test
public void testCheckoutChangeFileToEmptyDirs() throws Exception {
String fname = "was_file";
Git git = Git.wrap(db);
// Add a file
File file = writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
git.commit().setMessage("Added file").call();
// replace file with directory containing only directories, no files
FileUtils.delete(file);
FileUtils.mkdirs(new File(file, "dummyDir"));
assertTrue("File must be a directory now", file.isDirectory());
assertFalse("Must not delete non empty directory", file.delete());
assertWorkDir(mkmap(fname + "/dummyDir", "/"));
// revert path to HEAD state
git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
assertWorkDir(mkmap(fname, "a"));
Status st = git.status().call();
assertTrue(st.isClean());
}
@Test
public void testCheckoutChangeFileToNonEmptyDirs() throws Exception {
String fname = "was_file";
Git git = Git.wrap(db);
// Add a file
File file = writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
git.commit().setMessage("Added file").call();
assertWorkDir(mkmap(fname, "a"));
// replace file with directory containing only directories, no files
FileUtils.delete(file);
// create but do not add a file in the new directory to the index
writeTrashFile(fname + "/dir1", "file1", "c");
// create but do not add a file in the new directory to the index
writeTrashFile(fname + "/dir2", "file2", "d");
assertTrue("File must be a directory now", file.isDirectory());
assertFalse("Must not delete non empty directory", file.delete());
// 2 extra files are created
assertWorkDir(
mkmap(fname + "/dir1/file1", "c", fname + "/dir2/file2", "d"));
// revert path to HEAD state
git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
// expect only the one added to the index
assertWorkDir(mkmap(fname, "a"));
Status st = git.status().call();
assertTrue(st.isClean());
}
@Test
public void testCheckoutChangeFileToNonEmptyDirsAndNewIndexEntry()
throws Exception {
String fname = "was_file";
Git git = Git.wrap(db);
// Add a file
File file = writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
git.commit().setMessage("Added file").call();
assertWorkDir(mkmap(fname, "a"));
// replace file with directory containing only directories, no files
FileUtils.delete(file);
// create and add a file in the new directory to the index
writeTrashFile(fname + "/dir", "file1", "c");
git.add().addFilepattern(fname + "/dir/file1").call();
// create but do not add a file in the new directory to the index
writeTrashFile(fname + "/dir", "file2", "d");
assertTrue("File must be a directory now", file.isDirectory());
assertFalse("Must not delete non empty directory", file.delete());
// 2 extra files are created
assertWorkDir(
mkmap(fname + "/dir/file1", "c", fname + "/dir/file2", "d"));
// revert path to HEAD state
git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
assertWorkDir(mkmap(fname, "a"));
Status st = git.status().call();
assertFalse(st.isClean());
assertEquals(1, st.getAdded().size());
assertTrue(st.getAdded().contains(fname + "/dir/file1"));
}
@Test
public void testCheckoutOutChangesAutoCRLFfalse() throws IOException {
setupCase(mk("foo"), mkmap("foo/bar", "foo\nbar"), mk("foo"));
@ -1023,6 +1316,102 @@ public class DirCacheCheckoutTest extends RepositoryTestCase {
assertTrue(git.status().call().isClean());
}
@Test
public void testOverwriteUntrackedFileModeChange()
throws IOException, GitAPIException {
String fname = "file.txt";
Git git = Git.wrap(db);
// Add a file
File file = writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
git.commit().setMessage("create file").call();
assertWorkDir(mkmap(fname, "a"));
// Create branch
git.branchCreate().setName("side").call();
// Switch branches
git.checkout().setName("side").call();
// replace file with directory containing files
FileUtils.delete(file);
// create and add a file in the new directory to the index
writeTrashFile(fname + "/dir1", "file1", "c");
git.add().addFilepattern(fname + "/dir1/file1").call();
// create but do not add a file in the new directory to the index
writeTrashFile(fname + "/dir2", "file2", "d");
assertTrue("File must be a directory now", file.isDirectory());
assertFalse("Must not delete non empty directory", file.delete());
// 2 extra files are created
assertWorkDir(
mkmap(fname + "/dir1/file1", "c", fname + "/dir2/file2", "d"));
try {
git.checkout().setName("master").call();
fail("did not throw exception");
} catch (Exception e) {
// 2 extra files are still there
assertWorkDir(mkmap(fname + "/dir1/file1", "c",
fname + "/dir2/file2", "d"));
}
}
@Test
public void testOverwriteUntrackedLinkModeChange()
throws Exception {
String fname = "file.txt";
Git git = Git.wrap(db);
// Add a file
writeTrashFile(fname, "a");
git.add().addFilepattern(fname).call();
// Add a link to file
String linkName = "link";
File link = writeLink(linkName, fname).toFile();
git.add().addFilepattern(linkName).call();
git.commit().setMessage("Added file and link").call();
assertWorkDir(mkmap(linkName, "a", fname, "a"));
// Create branch
git.branchCreate().setName("side").call();
// Switch branches
git.checkout().setName("side").call();
// replace link with directory containing files
FileUtils.delete(link);
// create and add a file in the new directory to the index
writeTrashFile(linkName + "/dir1", "file1", "c");
git.add().addFilepattern(linkName + "/dir1/file1").call();
// create but do not add a file in the new directory to the index
writeTrashFile(linkName + "/dir2", "file2", "d");
assertTrue("Link must be a directory now", link.isDirectory());
assertFalse("Must not delete non empty directory", link.delete());
// 2 extra files are created
assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c",
linkName + "/dir2/file2", "d"));
try {
git.checkout().setName("master").call();
fail("did not throw exception");
} catch (Exception e) {
// 2 extra files are still there
assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c",
linkName + "/dir2/file2", "d"));
}
}
@Test
public void testFileModeChangeWithNoContentChangeUpdate() throws Exception {
if (!FS.DETECTED.supportsExecute())
@ -1219,7 +1608,8 @@ public class DirCacheCheckoutTest extends RepositoryTestCase {
assertNotNull(git.checkout().setName(Constants.MASTER).call());
}
public void assertWorkDir(HashMap<String, String> i) throws CorruptObjectException,
public void assertWorkDir(Map<String, String> i)
throws CorruptObjectException,
IOException {
TreeWalk walk = new TreeWalk(db);
walk.setRecursive(false);

8
org.eclipse.jgit/.settings/.api_filters

@ -21,4 +21,12 @@
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/util/FileUtils.java" type="org.eclipse.jgit.util.FileUtils">
<filter id="338792546">
<message_arguments>
<message_argument value="org.eclipse.jgit.util.FileUtils"/>
<message_argument value="createSymLink(File, String)"/>
</message_arguments>
</filter>
</resource>
</component>

2
org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java

@ -457,7 +457,7 @@ public class CheckoutCommand extends GitCommand<Ref> {
private void checkoutPath(DirCacheEntry entry, ObjectReader reader) {
try {
DirCacheCheckout.checkoutEntry(repo, entry, reader);
DirCacheCheckout.checkoutEntry(repo, entry, reader, true);
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().checkoutConflictWithFile,

2
org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java

@ -357,7 +357,7 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
private void checkoutPath(DirCacheEntry entry, ObjectReader reader) {
try {
DirCacheCheckout.checkoutEntry(repo, entry, reader);
DirCacheCheckout.checkoutEntry(repo, entry, reader, true);
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().checkoutConflictWithFile,

55
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java vendored

@ -447,7 +447,7 @@ public class DirCacheCheckout {
for (String path : updated.keySet()) {
DirCacheEntry entry = dc.getEntry(path);
if (!FileMode.GITLINK.equals(entry.getRawMode()))
checkoutEntry(repo, entry, objectReader);
checkoutEntry(repo, entry, objectReader, false);
}
// commit the index builder - a new index is persisted
@ -1127,6 +1127,13 @@ public class DirCacheCheckout {
* final filename.
*
* <p>
* <b>Note:</b> if the entry path on local file system exists as a non-empty
* directory, and the target entry type is a link or file, the checkout will
* fail with {@link IOException} since existing non-empty directory cannot
* be renamed to file or link without deleting it recursively.
* </p>
*
* <p>
* TODO: this method works directly on File IO, we may need another
* abstraction (like WorkingTreeIterator). This way we could tell e.g.
* Eclipse that Files in the workspace got changed
@ -1143,6 +1150,42 @@ public class DirCacheCheckout {
*/
public static void checkoutEntry(Repository repo, DirCacheEntry entry,
ObjectReader or) throws IOException {
checkoutEntry(repo, entry, or, false);
}
/**
* Updates the file in the working tree with content and mode from an entry
* in the index. The new content is first written to a new temporary file in
* the same directory as the real file. Then that new file is renamed to the
* final filename.
*
* <p>
* <b>Note:</b> if the entry path on local file system exists as a file, it
* will be deleted and if it exists as a directory, it will be deleted
* recursively, independently if has any content.
* </p>
*
* <p>
* TODO: this method works directly on File IO, we may need another
* abstraction (like WorkingTreeIterator). This way we could tell e.g.
* Eclipse that Files in the workspace got changed
* </p>
*
* @param repo
* repository managing the destination work tree.
* @param entry
* the entry containing new mode and content
* @param or
* object reader to use for checkout
* @param deleteRecursive
* true to recursively delete final path if it exists on the file
* system
*
* @throws IOException
* @since 4.2
*/
public static void checkoutEntry(Repository repo, DirCacheEntry entry,
ObjectReader or, boolean deleteRecursive) throws IOException {
ObjectLoader ol = or.open(entry.getObjectId());
File f = new File(repo.getWorkTree(), entry.getPathString());
File parentDir = f.getParentFile();
@ -1153,6 +1196,9 @@ public class DirCacheCheckout {
&& opt.getSymLinks() == SymLinks.TRUE) {
byte[] bytes = ol.getBytes();
String target = RawParseUtils.decode(bytes);
if (deleteRecursive && f.isDirectory()) {
FileUtils.delete(f, FileUtils.RECURSIVE);
}
fs.createSymLink(f, target);
entry.setLength(bytes.length);
entry.setLastModified(fs.lastModified(f));
@ -1183,11 +1229,18 @@ public class DirCacheCheckout {
}
}
try {
if (deleteRecursive && f.isDirectory()) {
FileUtils.delete(f, FileUtils.RECURSIVE);
}
FileUtils.rename(tmpFile, f);
} catch (IOException e) {
throw new IOException(MessageFormat.format(
JGitText.get().renameFileFailed, tmpFile.getPath(),
f.getPath()));
} finally {
if (tmpFile.exists()) {
FileUtils.delete(tmpFile);
}
}
entry.setLastModified(f.lastModified());
}

13
org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java

@ -399,20 +399,25 @@ public class FileUtils {
*
* @param path
* @param target
* @return path to the created link
* @throws IOException
* @since 3.0
* @since 4.2
*/
public static void createSymLink(File path, String target)
public static Path createSymLink(File path, String target)
throws IOException {
Path nioPath = path.toPath();
if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS)) {
Files.delete(nioPath);
if (Files.isRegularFile(nioPath)) {
delete(path);
} else {
delete(path, EMPTY_DIRECTORIES_ONLY | RECURSIVE);
}
}
if (SystemReader.getInstance().isWindows()) {
target = target.replace('/', '\\');
}
Path nioTarget = new File(target).toPath();
Files.createSymbolicLink(nioPath, nioTarget);
return Files.createSymbolicLink(nioPath, nioTarget);
}
/**

Loading…
Cancel
Save