diff options
Diffstat (limited to 'org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java')
-rw-r--r-- | org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java | 1569 |
1 files changed, 1251 insertions, 318 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java index 7a115e1076..0fafcd6a36 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java @@ -2,44 +2,17 @@ * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> * Copyright (C) 2008-2011, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2008-2011, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2010-2011, Christian Halstrick <christian.halstrick@sap.com> - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2010, 2022 Christian Halstrick <christian.halstrick@sap.com> and others * * 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 + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.lib; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -65,19 +38,27 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.NoFilepatternException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheEditor; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.CheckoutConflictException; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.events.ChangeRecorder; +import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; import org.eclipse.jgit.revwalk.RevCommit; +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.util.FS; +import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.StringUtils; +import org.junit.Assume; import org.junit.Test; public class DirCacheCheckoutTest extends RepositoryTestCase { @@ -111,7 +92,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { return dco.getRemoved(); } - private Map<String, ObjectId> getUpdated() { + private Map<String, CheckoutMetadata> getUpdated() { return dco.getUpdated(); } @@ -127,7 +108,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if ((args.length % 2) > 0) throw new IllegalArgumentException("needs to be pairs"); - HashMap<String, String> map = new HashMap<String, String>(); + HashMap<String, String> map = new HashMap<>(); for (int i = 0; i < args.length; i += 2) { map.put(args[i], args[i + 1]); } @@ -138,59 +119,83 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { @Test public void testResetHard() throws IOException, NoFilepatternException, GitAPIException { - Git git = new Git(db); - writeTrashFile("f", "f()"); - writeTrashFile("D/g", "g()"); - git.add().addFilepattern(".").call(); - git.commit().setMessage("inital").call(); - assertIndex(mkmap("f", "f()", "D/g", "g()")); - - git.branchCreate().setName("topic").call(); - - writeTrashFile("f", "f()\nmaster"); - writeTrashFile("D/g", "g()\ng2()"); - writeTrashFile("E/h", "h()"); - git.add().addFilepattern(".").call(); - RevCommit master = git.commit().setMessage("master-1").call(); - assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); - - checkoutBranch("refs/heads/topic"); - assertIndex(mkmap("f", "f()", "D/g", "g()")); - - writeTrashFile("f", "f()\nside"); - assertTrue(new File(db.getWorkTree(), "D/g").delete()); - writeTrashFile("G/i", "i()"); - git.add().addFilepattern(".").call(); - git.add().addFilepattern(".").setUpdate(true).call(); - RevCommit topic = git.commit().setMessage("topic-1").call(); - assertIndex(mkmap("f", "f()\nside", "G/i", "i()")); - - writeTrashFile("untracked", "untracked"); - - resetHard(master); - assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); - resetHard(topic); - assertIndex(mkmap("f", "f()\nside", "G/i", "i()")); - assertWorkDir(mkmap("f", "f()\nside", "G/i", "i()", "untracked", - "untracked")); - - assertEquals(MergeStatus.CONFLICTING, git.merge().include(master) - .call().getMergeStatus()); - assertEquals( - "[D/g, mode:100644, stage:1][D/g, mode:100644, stage:3][E/h, mode:100644][G/i, mode:100644][f, mode:100644, stage:1][f, mode:100644, stage:2][f, mode:100644, stage:3]", - indexState(0)); - - resetHard(master); - assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); - assertWorkDir(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", - "h()", "untracked", "untracked")); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + writeTrashFile("f", "f()"); + writeTrashFile("D/g", "g()"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("inital").call(); + assertIndex(mkmap("f", "f()", "D/g", "g()")); + recorder.assertNoEvent(); + git.branchCreate().setName("topic").call(); + recorder.assertNoEvent(); + + writeTrashFile("f", "f()\nmaster"); + writeTrashFile("D/g", "g()\ng2()"); + writeTrashFile("E/h", "h()"); + git.add().addFilepattern(".").call(); + RevCommit master = git.commit().setMessage("master-1").call(); + assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); + recorder.assertNoEvent(); + + checkoutBranch("refs/heads/topic"); + assertIndex(mkmap("f", "f()", "D/g", "g()")); + recorder.assertEvent(new String[] { "f", "D/g" }, + new String[] { "E/h" }); + + writeTrashFile("f", "f()\nside"); + assertTrue(new File(db.getWorkTree(), "D/g").delete()); + writeTrashFile("G/i", "i()"); + git.add().addFilepattern(".").call(); + git.add().addFilepattern(".").setUpdate(true).call(); + RevCommit topic = git.commit().setMessage("topic-1").call(); + assertIndex(mkmap("f", "f()\nside", "G/i", "i()")); + recorder.assertNoEvent(); + + writeTrashFile("untracked", "untracked"); + + resetHard(master); + assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); + recorder.assertEvent(new String[] { "f", "D/g", "E/h" }, + new String[] { "G", "G/i" }); + + resetHard(topic); + assertIndex(mkmap("f", "f()\nside", "G/i", "i()")); + assertWorkDir(mkmap("f", "f()\nside", "G/i", "i()", "untracked", + "untracked")); + recorder.assertEvent(new String[] { "f", "G/i" }, + new String[] { "D", "D/g", "E", "E/h" }); + + assertEquals(MergeStatus.CONFLICTING, git.merge().include(master) + .call().getMergeStatus()); + assertEquals( + "[D/g, mode:100644, stage:1][D/g, mode:100644, stage:3][E/h, mode:100644][G/i, mode:100644][f, mode:100644, stage:1][f, mode:100644, stage:2][f, mode:100644, stage:3]", + indexState(0)); + recorder.assertEvent(new String[] { "f", "D/g", "E/h" }, + ChangeRecorder.EMPTY); + + resetHard(master); + assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); + assertWorkDir(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", + "h()", "untracked", "untracked")); + recorder.assertEvent(new String[] { "f", "D/g" }, + new String[] { "G", "G/i" }); + + } finally { + if (handle != null) { + handle.remove(); + } + } } /** * Reset hard from unclean condition. * <p> - * WorkDir: Empty <br/> - * Index: f/g <br/> + * WorkDir: Empty <br> + * Index: f/g <br> * Merge: x * * @throws Exception @@ -198,21 +203,32 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { @Test public void testResetHardFromIndexEntryWithoutFileToTreeWithoutFile() throws Exception { - Git git = new Git(db); - writeTrashFile("x", "x"); - git.add().addFilepattern("x").call(); - RevCommit id1 = git.commit().setMessage("c1").call(); - - writeTrashFile("f/g", "f/g"); - git.rm().addFilepattern("x").call(); - git.add().addFilepattern("f/g").call(); - git.commit().setMessage("c2").call(); - deleteTrashFile("f/g"); - deleteTrashFile("f"); - - // The actual test - git.reset().setMode(ResetType.HARD).setRef(id1.getName()).call(); - assertIndex(mkmap("x", "x")); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + writeTrashFile("x", "x"); + git.add().addFilepattern("x").call(); + RevCommit id1 = git.commit().setMessage("c1").call(); + + writeTrashFile("f/g", "f/g"); + git.rm().addFilepattern("x").call(); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "x" }); + git.add().addFilepattern("f/g").call(); + git.commit().setMessage("c2").call(); + deleteTrashFile("f/g"); + deleteTrashFile("f"); + + // The actual test + git.reset().setMode(ResetType.HARD).setRef(id1.getName()).call(); + assertIndex(mkmap("x", "x")); + recorder.assertEvent(new String[] { "x" }, ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } + } } /** @@ -222,14 +238,134 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { */ @Test public void testInitialCheckout() throws Exception { - Git git = new Git(db); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db); + TestRepository<Repository> db_t = new TestRepository<>(db)) { + db.incrementOpen(); + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + BranchBuilder master = db_t.branch("master"); + master.commit().add("f", "1").message("m0").create(); + assertFalse(new File(db.getWorkTree(), "f").exists()); + git.checkout().setName("master").call(); + assertTrue(new File(db.getWorkTree(), "f").exists()); + recorder.assertEvent(new String[] { "f" }, ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + private void checkoutLineEndings(String inIndex, String expected, + String attributes) throws Exception { + try (Git git = new Git(db); + TestRepository<Repository> db_t = new TestRepository<>(db)) { + db.incrementOpen(); + BranchBuilder master = db_t.branch("master"); + master.commit().add("f", inIndex).message("m0").create(); + if (!StringUtils.isEmptyOrNull(attributes)) { + master.commit().add(".gitattributes", attributes) + .message("attributes").create(); + } + File f = new File(db.getWorkTree(), "f"); + assertFalse(f.exists()); + git.checkout().setName("master").call(); + assertTrue(f.exists()); + checkFile(f, expected); + } + } + + @Test + public void testCheckoutWithCRLF() throws Exception { + checkoutLineEndings("first line\r\nsecond line\r\n", + "first line\r\nsecond line\r\n", null); + } + + @Test + public void testCheckoutWithCRLFAuto() throws Exception { + checkoutLineEndings("first line\r\nsecond line\r\n", + "first line\r\nsecond line\r\n", "f text=auto"); + } + + @Test + public void testCheckoutWithCRLFAutoEolLf() throws Exception { + checkoutLineEndings("first line\r\nsecond line\r\n", + "first line\r\nsecond line\r\n", "f text=auto eol=lf"); + } + + @Test + public void testCheckoutWithCRLFAutoEolNative() throws Exception { + checkoutLineEndings("first line\r\nsecond line\r\n", + "first line\r\nsecond line\r\n", "f text=auto eol=native"); + } + + @Test + public void testCheckoutWithCRLFAutoEolCrLf() throws Exception { + checkoutLineEndings("first line\r\nsecond line\r\n", + "first line\r\nsecond line\r\n", "f text=auto eol=crlf"); + } + + @Test + public void testCheckoutWithLF() throws Exception { + checkoutLineEndings("first line\nsecond line\n", + "first line\nsecond line\n", null); + } + + @Test + public void testCheckoutWithLFAuto() throws Exception { + String expected = String.format("first line%nsecond line%n"); + checkoutLineEndings("first line\nsecond line\n", expected, + "f text=auto"); + } + + @Test + public void testCheckoutWithLFAutoEolLf() throws Exception { + checkoutLineEndings("first line\nsecond line\n", + "first line\nsecond line\n", "f text=auto eol=lf"); + } + + @Test + public void testCheckoutWithLFAutoEolNative() throws Exception { + String expected = String.format("first line%nsecond line%n"); + checkoutLineEndings( + "first line\nsecond line\n", expected, + "f text=auto eol=native"); + } + + @Test + public void testCheckoutWithLFAutoEolCrLf() throws Exception { + checkoutLineEndings("first line\nsecond line\n", + "first line\r\nsecond line\r\n", "f text=auto eol=crlf"); + } + + @Test + public void testCheckoutMixedAutoEolCrLf() throws Exception { + checkoutLineEndings("first line\nsecond line\r\n", + "first line\nsecond line\r\n", "f text=auto eol=crlf"); + } + + @Test + public void testCheckoutMixedAutoEolLf() throws Exception { + checkoutLineEndings("first line\nsecond line\r\n", + "first line\nsecond line\r\n", "f text=auto eol=lf"); + } - TestRepository<Repository> db_t = new TestRepository<Repository>(db); - BranchBuilder master = db_t.branch("master"); - master.commit().add("f", "1").message("m0").create(); - assertFalse(new File(db.getWorkTree(), "f").exists()); - git.checkout().setName("master").call(); - assertTrue(new File(db.getWorkTree(), "f").exists()); + @Test + public void testCheckoutMixedTextCrLf() throws Exception { + // Huh? Is this a bug in git? Both git 2.18.0 and git 2.33.0 do + // write the file with CRLF (and consequently report the file as + // modified in "git status" after check-out), however the CRLF in the + // repository is _not_ replaced by LF with eol=lf (see test below). + checkoutLineEndings("first line\nsecond line\r\n", + "first line\r\nsecond line\r\n", "f text eol=crlf"); + } + + @Test + public void testCheckoutMixedTextLf() throws Exception { + checkoutLineEndings("first line\nsecond line\r\nfoo", + "first line\nsecond line\r\nfoo", "f text eol=lf"); } private DirCacheCheckout resetHard(RevCommit commit) @@ -259,15 +395,13 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { assertTrue("unexpected content for path " + path + " in index. Expected: <" + expectedValue + ">", Arrays.equals(db.open(read.getEntry(j).getObjectId()) - .getCachedBytes(), i.get(path).getBytes())); + .getCachedBytes(), i.get(path).getBytes(UTF_8))); } } @Test public void testRules1thru3_NoIndexEntry() throws IOException { ObjectId head = buildTree(mk("foo")); - TreeWalk tw = TreeWalk.forPath(db, "foo", head); - ObjectId objectId = tw.getObjectId(0); ObjectId merge = db.newObjectInserter().insert(Constants.OBJ_TREE, new byte[0]); @@ -277,10 +411,9 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { prescanTwoTrees(merge, head); - assertEquals(objectId, getUpdated().get("foo")); + assertTrue(getUpdated().containsKey("foo")); merge = buildTree(mkmap("foo", "a")); - tw = TreeWalk.forPath(db, "foo", merge); prescanTwoTrees(head, merge); @@ -300,9 +433,11 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { DirCacheEditor editor = dirCache.editor(); for (java.util.Map.Entry<String,String> e : indexEntries.entrySet()) { writeTrashFile(e.getKey(), e.getValue()); - ObjectInserter inserter = db.newObjectInserter(); - final ObjectId id = inserter.insert(Constants.OBJ_BLOB, + ObjectId id; + try (ObjectInserter inserter = db.newObjectInserter()) { + id = inserter.insert(Constants.OBJ_BLOB, Constants.encode(e.getValue())); + } editor.add(new DirCacheEditor.DeletePath(e.getKey())); editor.add(new DirCacheEditor.PathEdit(e.getKey()) { @Override @@ -357,7 +492,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { ObjectId genSha1(String data) { try (ObjectInserter w = db.newObjectInserter()) { - ObjectId id = w.insert(Constants.OBJ_BLOB, data.getBytes()); + ObjectId id = w.insert(Constants.OBJ_BLOB, data.getBytes(UTF_8)); w.flush(); return id; } catch (IOException e) { @@ -375,7 +510,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { // rules 4 and 5 HashMap<String, String> idxMap; - idxMap = new HashMap<String, String>(); + idxMap = new HashMap<>(); idxMap.put("foo", "foo"); setupCase(null, null, idxMap); go(); @@ -385,7 +520,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { assertTrue(getConflicts().isEmpty()); // rules 6 and 7 - idxMap = new HashMap<String, String>(); + idxMap = new HashMap<>(); idxMap.put("foo", "foo"); setupCase(null, idxMap, idxMap); go(); @@ -394,7 +529,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { // rules 8 and 9 HashMap<String, String> mergeMap; - mergeMap = new HashMap<String, String>(); + mergeMap = new HashMap<>(); mergeMap.put("foo", "merge"); setupCase(null, mergeMap, idxMap); @@ -406,7 +541,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { // rule 10 - HashMap<String, String> headMap = new HashMap<String, String>(); + HashMap<String, String> headMap = new HashMap<>(); headMap.put("foo", "foo"); setupCase(headMap, null, idxMap); go(); @@ -796,6 +931,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { fail("didn't get the expected exception"); } catch (CheckoutConflictException e) { assertConflict("foo"); + assertEquals("foo", e.getConflictingFiles()[0]); assertWorkDir(mkmap("foo", "bar", "other", "other")); assertIndex(mk("other")); } @@ -879,10 +1015,12 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { "e/g3")); try { checkout(); + fail("did not throw CheckoutConflictException"); } catch (CheckoutConflictException e) { assertWorkDir(mkmap("a", "a", "b/c", "b/c", "d", "d", "e/f", "e/f", "e/g", "e/g3")); assertConflict("e/g"); + assertEquals("e/g", e.getConflictingFiles()[0]); } } @@ -923,6 +1061,383 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { } @Test + public void testCheckoutChangeLinkToEmptyDir() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + String fname = "was_file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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, "/")); + recorder.assertNoEvent(); + + // revert both paths to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname) + .addPath(linkName).call(); + + assertWorkDir(mkmap(fname, "a", linkName, "a")); + recorder.assertEvent(new String[] { fname, linkName }, + ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test + public void testCheckoutChangeLinkToEmptyDirs() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + String fname = "was_file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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", "/")); + recorder.assertNoEvent(); + + // revert both paths to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname) + .addPath(linkName).call(); + + assertWorkDir(mkmap(fname, "a", linkName, "a")); + recorder.assertEvent(new String[] { fname, linkName }, + ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test + public void testCheckoutChangeLinkToNonEmptyDirs() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + String fname = "file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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")); + recorder.assertNoEvent(); + + // 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")); + recorder.assertEvent(new String[] { linkName }, + ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test + public void testCheckoutChangeLinkToNonEmptyDirsAndNewIndexEntry() + throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + String fname = "file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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")); + recorder.assertNoEvent(); + + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(linkName) + .call(); + + // original file and link + assertWorkDir(mkmap(linkName, "a", fname, "a")); + recorder.assertEvent(new String[] { linkName }, + ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test + public void testCheckoutChangeFileToEmptyDir() throws Exception { + String fname = "was_file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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, "/")); + recorder.assertNoEvent(); + + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); + assertWorkDir(mkmap(fname, "a")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test + public void testCheckoutChangeFileToEmptyDirs() throws Exception { + String fname = "was_file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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", "/")); + recorder.assertNoEvent(); + + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); + assertWorkDir(mkmap(fname, "a")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test + public void testCheckoutChangeFileToNonEmptyDirs() throws Exception { + String fname = "was_file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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")); + recorder.assertNoEvent(); + + // 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")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test + public void testCheckoutChangeFileToNonEmptyDirsAndNewIndexEntry() + throws Exception { + String fname = "was_file"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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")); + recorder.assertNoEvent(); + + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); + assertWorkDir(mkmap(fname, "a")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } + + @Test public void testCheckoutOutChangesAutoCRLFfalse() throws IOException { setupCase(mk("foo"), mkmap("foo/bar", "foo\nbar"), mk("foo")); checkout(); @@ -983,35 +1498,174 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { } @Test + public void testDontOverwriteEmptyFolder() throws IOException { + setupCase(mk("foo"), mk("foo"), mk("foo")); + FileUtils.mkdir(new File(db.getWorkTree(), "d")); + checkout(); + assertWorkDir(mkmap("foo", "foo", "d", "/")); + } + + @Test public void testOverwriteUntrackedIgnoredFile() throws IOException, GitAPIException { String fname="file.txt"; - Git git = Git.wrap(db); - - // Add a file - writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("create file").call(); - - // Create branch - git.branchCreate().setName("side").call(); - - // Modify file - writeTrashFile(fname, "b"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("modify file").call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("create file").call(); + + // Create branch + git.branchCreate().setName("side").call(); + + // Modify file + writeTrashFile(fname, "b"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("modify file").call(); + recorder.assertNoEvent(); + + // Switch branches + git.checkout().setName("side").call(); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + git.rm().addFilepattern(fname).call(); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { fname }); + writeTrashFile(".gitignore", fname); + git.add().addFilepattern(".gitignore").call(); + git.commit().setMessage("delete and ignore file").call(); + + writeTrashFile(fname, "Something different"); + recorder.assertNoEvent(); + git.checkout().setName("master").call(); + assertWorkDir(mkmap(fname, "b")); + recorder.assertEvent(new String[] { fname }, + new String[] { ".gitignore" }); + assertTrue(git.status().call().isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } + } - // Switch branches - git.checkout().setName("side").call(); - git.rm().addFilepattern(fname).call(); - writeTrashFile(".gitignore", fname); - git.add().addFilepattern(".gitignore").call(); - git.commit().setMessage("delete and ignore file").call(); + @Test + public void testOverwriteUntrackedFileModeChange() + throws IOException, GitAPIException { + String fname = "file.txt"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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(); + recorder.assertNoEvent(); + + // 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")); + } + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } + } + } - writeTrashFile(fname, "Something different"); - git.checkout().setName("master").call(); - assertWorkDir(mkmap(fname, "b")); - assertTrue(git.status().call().isClean()); + @Test + public void testOverwriteUntrackedLinkModeChange() + throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + String fname = "file.txt"; + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // 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(); + recorder.assertNoEvent(); + + // 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")); + } + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test @@ -1019,36 +1673,47 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); - - // Create branch - git.branchCreate().setName("b1").call(); - - // Make file executable - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit2").call(); - - // Verify executable and working directory is clean - Status status = git.status().call(); - assertTrue(status.getModified().isEmpty()); - assertTrue(status.getChanged().isEmpty()); - assertTrue(db.getFS().canExecute(file)); - - // Switch branches - git.checkout().setName("b1").call(); - - // Verify not executable and working directory is clean - status = git.status().call(); - assertTrue(status.getModified().isEmpty()); - assertTrue(status.getChanged().isEmpty()); - assertFalse(db.getFS().canExecute(file)); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Make file executable + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit2").call(); + recorder.assertNoEvent(); + + // Verify executable and working directory is clean + Status status = git.status().call(); + assertTrue(status.getModified().isEmpty()); + assertTrue(status.getChanged().isEmpty()); + assertTrue(db.getFS().canExecute(file)); + + // Switch branches + git.checkout().setName("b1").call(); + + // Verify not executable and working directory is clean + status = git.status().call(); + assertTrue(status.getModified().isEmpty()); + assertTrue(status.getChanged().isEmpty()); + assertFalse(db.getFS().canExecute(file)); + recorder.assertEvent(new String[] { "file.txt" }, + ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test @@ -1056,41 +1721,50 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); - - // Create branch - git.branchCreate().setName("b1").call(); - - // Make file executable - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit2").call(); - - // Verify executable and working directory is clean - Status status = git.status().call(); - assertTrue(status.getModified().isEmpty()); - assertTrue(status.getChanged().isEmpty()); - assertTrue(db.getFS().canExecute(file)); - - writeTrashFile("file.txt", "b"); - - // Switch branches - CheckoutCommand checkout = git.checkout().setName("b1"); - try { - checkout.call(); - fail("Checkout exception not thrown"); - } catch (org.eclipse.jgit.api.errors.CheckoutConflictException e) { - CheckoutResult result = checkout.getResult(); - assertNotNull(result); - assertNotNull(result.getConflictList()); - assertEquals(1, result.getConflictList().size()); - assertTrue(result.getConflictList().contains("file.txt")); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Make file executable + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit2").call(); + + // Verify executable and working directory is clean + Status status = git.status().call(); + assertTrue(status.getModified().isEmpty()); + assertTrue(status.getChanged().isEmpty()); + assertTrue(db.getFS().canExecute(file)); + + writeTrashFile("file.txt", "b"); + + // Switch branches + CheckoutCommand checkout = git.checkout().setName("b1"); + try { + checkout.call(); + fail("Checkout exception not thrown"); + } catch (org.eclipse.jgit.api.errors.CheckoutConflictException e) { + CheckoutResult result = checkout.getResult(); + assertNotNull(result); + assertNotNull(result.getConflictList()); + assertEquals(1, result.getConflictList().size()); + assertTrue(result.getConflictList().contains("file.txt")); + } + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } } } @@ -1100,149 +1774,408 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); - - // Create branch - git.branchCreate().setName("b1").call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Create second commit and don't touch file + writeTrashFile("file2.txt", ""); + git.add().addFilepattern("file2.txt").call(); + git.commit().setMessage("commit2").call(); + + // stage a mode change + writeTrashFile("file.txt", "a"); + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + + // dirty the file + writeTrashFile("file.txt", "b"); + + assertEquals( + "[file.txt, mode:100755, content:a][file2.txt, mode:100644, content:]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "b", "file2.txt", "")); + recorder.assertNoEvent(); + + // Switch branches and check that the dirty file survived in + // worktree and index + git.checkout().setName("b1").call(); + assertEquals("[file.txt, mode:100755, content:a]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "b")); + recorder.assertEvent(ChangeRecorder.EMPTY, + new String[] { "file2.txt" }); + } finally { + if (handle != null) { + handle.remove(); + } + } + } - // Create second commit and don't touch file - writeTrashFile("file2.txt", ""); - git.add().addFilepattern("file2.txt").call(); - git.commit().setMessage("commit2").call(); + @Test + public void testDirtyFileModeEqualIndexMerge() + throws Exception { + if (!FS.DETECTED.supportsExecute()) + return; - // stage a mode change - writeTrashFile("file.txt", "a"); - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Create second commit with executable file + file = writeTrashFile("file.txt", "b"); + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit2").call(); + + // stage the same content as in the branch we want to switch to + writeTrashFile("file.txt", "a"); + db.getFS().setExecute(file, false); + git.add().addFilepattern("file.txt").call(); + + // dirty the file + writeTrashFile("file.txt", "c"); + db.getFS().setExecute(file, true); + + assertEquals("[file.txt, mode:100644, content:a]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "c")); + recorder.assertNoEvent(); + + // Switch branches and check that the dirty file survived in + // worktree + // and index + git.checkout().setName("b1").call(); + assertEquals("[file.txt, mode:100644, content:a]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "c")); + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } + } + } - // dirty the file - writeTrashFile("file.txt", "b"); + @Test + public void testFileModeChangeAndContentChangeNoConflict() throws Exception { + if (!FS.DETECTED.supportsExecute()) + return; - assertEquals( - "[file.txt, mode:100755, content:a][file2.txt, mode:100644, content:]", - indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "b", "file2.txt", "")); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add first file + File file1 = writeTrashFile("file1.txt", "a"); + git.add().addFilepattern("file1.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file1)); + + // Add second file + File file2 = writeTrashFile("file2.txt", "b"); + git.add().addFilepattern("file2.txt").call(); + git.commit().setMessage("commit2").call(); + assertFalse(db.getFS().canExecute(file2)); + recorder.assertNoEvent(); + + // Create branch from first commit + assertNotNull(git.checkout().setCreateBranch(true).setName("b1") + .setStartPoint(Constants.HEAD + "~1").call()); + recorder.assertEvent(ChangeRecorder.EMPTY, + new String[] { "file2.txt" }); + + // Change content and file mode in working directory and index + file1 = writeTrashFile("file1.txt", "c"); + db.getFS().setExecute(file1, true); + git.add().addFilepattern("file1.txt").call(); + + // Switch back to 'master' + assertNotNull(git.checkout().setName(Constants.MASTER).call()); + recorder.assertEvent(new String[] { "file2.txt" }, + ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } + } + } - // Switch branches and check that the dirty file survived in worktree - // and index - git.checkout().setName("b1").call(); - assertEquals("[file.txt, mode:100755, content:a]", indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "b")); + @Test(expected = CheckoutConflictException.class) + public void testFolderFileConflict() throws Exception { + RevCommit headCommit = commitFile("f/a", "initial content", "master"); + RevCommit checkoutCommit = commitFile("f/a", "side content", "side"); + FileUtils.delete(new File(db.getWorkTree(), "f"), FileUtils.RECURSIVE); + writeTrashFile("f", "file instead of folder"); + new DirCacheCheckout(db, headCommit.getTree(), db.lockDirCache(), + checkoutCommit.getTree()).checkout(); } @Test - public void testDirtyFileModeEqualIndexMerge() - throws Exception { - if (!FS.DETECTED.supportsExecute()) - return; + public void testMultipleContentConflicts() throws Exception { + commitFile("a", "initial content", "master"); + RevCommit headCommit = commitFile("b", "initial content", "master"); + commitFile("a", "side content", "side"); + RevCommit checkoutCommit = commitFile("b", "side content", "side"); + writeTrashFile("a", "changed content"); + writeTrashFile("b", "changed content"); - Git git = Git.wrap(db); + try { + new DirCacheCheckout(db, headCommit.getTree(), db.lockDirCache(), + checkoutCommit.getTree()).checkout(); + fail(); + } catch (CheckoutConflictException expected) { + assertEquals(2, expected.getConflictingFiles().length); + assertTrue(Arrays.asList(expected.getConflictingFiles()) + .contains("a")); + assertTrue(Arrays.asList(expected.getConflictingFiles()) + .contains("b")); + assertEquals("changed content", read("a")); + assertEquals("changed content", read("b")); + } + } - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); + @Test + public void testFolderFileAndContentConflicts() throws Exception { + RevCommit headCommit = commitFile("f/a", "initial content", "master"); + commitFile("b", "side content", "side"); + RevCommit checkoutCommit = commitFile("f/a", "side content", "side"); + FileUtils.delete(new File(db.getWorkTree(), "f"), FileUtils.RECURSIVE); + writeTrashFile("f", "file instead of a folder"); + writeTrashFile("b", "changed content"); - // Create branch - git.branchCreate().setName("b1").call(); + try { + new DirCacheCheckout(db, headCommit.getTree(), db.lockDirCache(), + checkoutCommit.getTree()).checkout(); + fail(); + } catch (CheckoutConflictException expected) { + assertEquals(2, expected.getConflictingFiles().length); + assertTrue(Arrays.asList(expected.getConflictingFiles()) + .contains("b")); + assertTrue(Arrays.asList(expected.getConflictingFiles()) + .contains("f")); + assertEquals("file instead of a folder", read("f")); + assertEquals("changed content", read("b")); + } + } - // Create second commit with executable file - file = writeTrashFile("file.txt", "b"); - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit2").call(); + @Test + public void testLongFilename() throws Exception { + char[] bytes = new char[253]; + Arrays.fill(bytes, 'f'); + String longFileName = new String(bytes); + // 1 + doit(mkmap(longFileName, "a"), mkmap(longFileName, "b"), + mkmap(longFileName, "a")); + writeTrashFile(longFileName, "a"); + checkout(); + assertNoConflicts(); + assertUpdated(longFileName); + } - // stage the same content as in the branch we want to switch to - writeTrashFile("file.txt", "a"); - db.getFS().setExecute(file, false); - git.add().addFilepattern("file.txt").call(); + @Test + public void testIgnoredDirectory() throws Exception { + writeTrashFile(".gitignore", "src/ignored"); + writeTrashFile("src/ignored/sub/foo.txt", "1"); + try (Git git = new Git(db)) { + git.add().addFilepattern(".").call(); + RevCommit commit = git.commit().setMessage("adding .gitignore") + .call(); + writeTrashFile("foo.txt", "2"); + writeTrashFile("zzz.txt", "3"); + git.add().addFilepattern("foo.txt").call(); + git.commit().setMessage("add file").call(); + assertEquals("Should not have entered ignored directory", 1, + resetHardAndCount(commit)); + } + } - // dirty the file - writeTrashFile("file.txt", "c"); - db.getFS().setExecute(file, true); + @Test + public void testIgnoredDirectoryWithTrackedContent() throws Exception { + writeTrashFile("src/ignored/sub/foo.txt", "1"); + try (Git git = new Git(db)) { + git.add().addFilepattern(".").call(); + git.commit().setMessage("adding foo.txt").call(); + writeTrashFile(".gitignore", "src/ignored"); + writeTrashFile("src/ignored/sub/foo.txt", "2"); + writeTrashFile("src/ignored/other/bar.txt", "3"); + git.add().addFilepattern(".").call(); + RevCommit commit = git.commit().setMessage("adding .gitignore") + .call(); + writeTrashFile("foo.txt", "2"); + writeTrashFile("zzz.txt", "3"); + git.add().addFilepattern("foo.txt").call(); + git.commit().setMessage("add file").call(); + File file = writeTrashFile("src/ignored/sub/foo.txt", "3"); + assertEquals("Should have entered ignored directory", 3, + resetHardAndCount(commit)); + checkFile(file, "2"); + } + } - assertEquals("[file.txt, mode:100644, content:a]", indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "c")); + @Test + public void testResetWithChangeInGitignore() throws Exception { + writeTrashFile(".gitignore", "src/ignored"); + writeTrashFile("src/ignored/sub/foo.txt", "1"); + try (Git git = new Git(db)) { + git.add().addFilepattern(".").call(); + RevCommit initial = git.commit().setMessage("initial").call(); + writeTrashFile("src/newignored/foo.txt", "2"); + writeTrashFile("src/.gitignore", "newignored"); + git.add().addFilepattern(".").call(); + RevCommit commit = git.commit().setMessage("newignored").call(); + assertEquals("Should not have entered src/newignored directory", 1, + resetHardAndCount(initial)); + assertEquals("Should have entered src/newignored directory", 2, + resetHardAndCount(commit)); + deleteTrashFile("src/.gitignore"); + git.rm().addFilepattern("src/.gitignore").call(); + RevCommit top = git.commit().setMessage("Unignore newignore") + .call(); + assertEquals("Should have entered src/newignored directory", 2, + resetHardAndCount(initial)); + assertEquals("Should have entered src/newignored directory", 2, + resetHardAndCount(commit)); + assertEquals("Should not have entered src/newignored directory", 1, + resetHardAndCount(top)); - // Switch branches and check that the dirty file survived in worktree - // and index - git.checkout().setName("b1").call(); - assertEquals("[file.txt, mode:100644, content:a]", indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "c")); + } } @Test - public void testFileModeChangeAndContentChangeNoConflict() throws Exception { - if (!FS.DETECTED.supportsExecute()) - return; + public void testCheckoutWithEmptyIndexDoesntOverwrite() throws Exception { + try (Git git = new Git(db); + TestRepository<Repository> db_t = new TestRepository<>(db)) { + db.incrementOpen(); + // prepare the commits + BranchBuilder master = db_t.branch("master"); + RevCommit mergeCommit = master.commit() + .add("p/x", "headContent") + .message("m0").create(); + master.commit().add("p/x", "headContent").message("m1").create(); + git.checkout().setName("master").call(); + + // empty index and write unsaved data in 'p' + git.rm().addFilepattern("p").call(); + writeTrashFile("p", "important data"); + + git.checkout().setName(mergeCommit.getName()).call(); + + assertEquals("", indexState(CONTENT)); + assertEquals("important data", read("p")); + } + } - Git git = Git.wrap(db); + private static class TestFileTreeIterator extends FileTreeIterator { - // Add first file - File file1 = writeTrashFile("file1.txt", "a"); - git.add().addFilepattern("file1.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file1)); + // For assertions only + private final int[] count; - // Add second file - File file2 = writeTrashFile("file2.txt", "b"); - git.add().addFilepattern("file2.txt").call(); - git.commit().setMessage("commit2").call(); - assertFalse(db.getFS().canExecute(file2)); + public TestFileTreeIterator(Repository repo, int[] count) { + super(repo); + this.count = count; + } - // Create branch from first commit - assertNotNull(git.checkout().setCreateBranch(true).setName("b1") - .setStartPoint(Constants.HEAD + "~1").call()); + protected TestFileTreeIterator(final WorkingTreeIterator p, + final File root, FS fs, FileModeStrategy fileModeStrategy, + int[] count) { + super(p, root, fs, fileModeStrategy); + this.count = count; + } - // Change content and file mode in working directory and index - file1 = writeTrashFile("file1.txt", "c"); - db.getFS().setExecute(file1, true); - git.add().addFilepattern("file1.txt").call(); + @Override + protected AbstractTreeIterator enterSubtree() { + count[0] += 1; + return new TestFileTreeIterator(this, + ((FileEntry) current()).getFile(), fs, fileModeStrategy, + count); + } + } - // Switch back to 'master' - assertNotNull(git.checkout().setName(Constants.MASTER).call()); + private int resetHardAndCount(RevCommit commit) throws Exception { + int[] callCount = { 0 }; + DirCache cache = db.lockDirCache(); + FileTreeIterator workingTreeIterator = new TestFileTreeIterator(db, + callCount); + try { + DirCacheCheckout checkout = new DirCacheCheckout(db, null, cache, + commit.getTree().getId(), workingTreeIterator); + checkout.setFailOnConflict(false); + checkout.checkout(); + } finally { + cache.unlock(); + } + return callCount[0]; } - 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(true); - walk.addTree(new FileTreeIterator(db)); - String expectedValue; - String path; - int nrFiles = 0; - FileTreeIterator ft; - while (walk.next()) { - ft = walk.getTree(0, FileTreeIterator.class); - path = ft.getEntryPathString(); - expectedValue = i.get(path); - assertNotNull("found unexpected file for path " + path - + " in workdir", expectedValue); - File file = new File(db.getWorkTree(), path); - assertTrue(file.exists()); - if (file.isFile()) { - FileInputStream is = new FileInputStream(file); - byte[] buffer = new byte[(int) file.length()]; - int offset = 0; - int numRead = 0; - while (offset < buffer.length - && (numRead = is.read(buffer, offset, buffer.length - - offset)) >= 0) { - offset += numRead; + try (TreeWalk walk = new TreeWalk(db)) { + walk.setRecursive(false); + walk.addTree(new FileTreeIterator(db)); + String expectedValue; + String path; + int nrFiles = 0; + FileTreeIterator ft; + while (walk.next()) { + ft = walk.getTree(0, FileTreeIterator.class); + path = ft.getEntryPathString(); + expectedValue = i.get(path); + File file = new File(db.getWorkTree(), path); + assertTrue(file.exists()); + if (file.isFile()) { + assertNotNull("found unexpected file for path " + path + + " in workdir", expectedValue); + try (FileInputStream is = new FileInputStream(file)) { + byte[] buffer = new byte[(int) file.length()]; + int offset = 0; + int numRead = 0; + while (offset < buffer.length + && (numRead = is.read(buffer, offset, + buffer.length - offset)) >= 0) { + offset += numRead; + } + assertArrayEquals( + "unexpected content for path " + path + + " in workDir. ", + buffer, i.get(path).getBytes(UTF_8)); + } + nrFiles++; + } else if (file.isDirectory()) { + String[] files = file.list(); + if (files != null && files.length == 0) { + assertEquals("found unexpected empty folder for path " + + path + " in workDir. ", "/", i.get(path)); + nrFiles++; + } + } + if (walk.isSubtree()) { + walk.enterSubtree(); } - is.close(); - assertArrayEquals("unexpected content for path " + path - + " in workDir. ", buffer, i.get(path).getBytes()); - nrFiles++; } + assertEquals("WorkDir has not the right size.", i.size(), nrFiles); } - assertEquals("WorkDir has not the right size.", i.size(), nrFiles); } } |