/* * Copyright (C) 2007, Dave Watson * Copyright (C) 2008-2011, Shawn O. Pearce * Copyright (C) 2008-2011, Robin Rosenberg * Copyright (C) 2010, 2020 Christian Halstrick and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * 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; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CheckoutResult; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeResult.MergeStatus; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.Status; 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 { private DirCacheCheckout dco; protected ObjectId theHead; protected ObjectId theMerge; private DirCache dirCache; private void prescanTwoTrees(ObjectId head, ObjectId merge) throws IllegalStateException, IOException { DirCache dc = db.lockDirCache(); try { dco = new DirCacheCheckout(db, head, dc, merge); dco.preScanTwoTrees(); } finally { dc.unlock(); } } private void checkout() throws IOException { DirCache dc = db.lockDirCache(); try { dco = new DirCacheCheckout(db, theHead, dc, theMerge); dco.checkout(); } finally { dc.unlock(); } } private List getRemoved() { return dco.getRemoved(); } private Map getUpdated() { return dco.getUpdated(); } private List getConflicts() { return dco.getConflicts(); } private static HashMap mk(String a) { return mkmap(a, a); } private static HashMap mkmap(String... args) { if ((args.length % 2) > 0) throw new IllegalArgumentException("needs to be pairs"); HashMap map = new HashMap<>(); for (int i = 0; i < args.length; i += 2) { map.put(args[i], args[i + 1]); } return map; } @Test public void testResetHard() throws IOException, NoFilepatternException, GitAPIException { 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. *

* WorkDir: Empty
* Index: f/g
* Merge: x * * @throws Exception */ @Test public void testResetHardFromIndexEntryWithoutFileToTreeWithoutFile() throws Exception { 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(); } } } /** * Test first checkout in a repo * * @throws Exception */ @Test public void testInitialCheckout() throws Exception { ChangeRecorder recorder = new ChangeRecorder(); ListenerHandle handle = null; try (Git git = new Git(db); TestRepository db_t = new TestRepository<>(db)) { 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 db_t = new TestRepository<>(db)) { 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 { checkoutLineEndings("first line\nsecond line\n", "first line\nsecond line\n", "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 { checkoutLineEndings( "first line\nsecond line\n", "first line\nsecond line\n" .replaceAll("\n", System.lineSeparator()), "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"); } private DirCacheCheckout resetHard(RevCommit commit) throws NoWorkTreeException, CorruptObjectException, IOException { DirCacheCheckout dc; dc = new DirCacheCheckout(db, null, db.lockDirCache(), commit.getTree()); dc.setFailOnConflict(true); assertTrue(dc.checkout()); return dc; } private void assertIndex(HashMap i) throws CorruptObjectException, IOException { String expectedValue; String path; DirCache read = DirCache.read(db.getIndexFile(), db.getFS()); assertEquals("Index has not the right size.", i.size(), read.getEntryCount()); for (int j = 0; j < read.getEntryCount(); j++) { path = read.getEntry(j).getPathString(); expectedValue = i.get(path); assertNotNull("found unexpected entry for path " + path + " in index", expectedValue); assertTrue("unexpected content for path " + path + " in index. Expected: <" + expectedValue + ">", Arrays.equals(db.open(read.getEntry(j).getObjectId()) .getCachedBytes(), i.get(path).getBytes(UTF_8))); } } @Test public void testRules1thru3_NoIndexEntry() throws IOException { ObjectId head = buildTree(mk("foo")); ObjectId merge = db.newObjectInserter().insert(Constants.OBJ_TREE, new byte[0]); prescanTwoTrees(head, merge); assertTrue(getRemoved().contains("foo")); prescanTwoTrees(merge, head); assertTrue(getUpdated().containsKey("foo")); merge = buildTree(mkmap("foo", "a")); prescanTwoTrees(head, merge); assertConflict("foo"); } void setupCase(HashMap headEntries, HashMap mergeEntries, HashMap indexEntries) throws IOException { theHead = buildTree(headEntries); theMerge = buildTree(mergeEntries); buildIndex(indexEntries); } private void buildIndex(HashMap indexEntries) throws IOException { dirCache = new DirCache(db.getIndexFile(), db.getFS()); if (indexEntries != null) { assertTrue(dirCache.lock()); DirCacheEditor editor = dirCache.editor(); for (java.util.Map.Entry e : indexEntries.entrySet()) { writeTrashFile(e.getKey(), e.getValue()); 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 public void apply(DirCacheEntry ent) { ent.setFileMode(FileMode.REGULAR_FILE); ent.setObjectId(id); ent.setUpdateNeeded(false); } }); } assertTrue(editor.commit()); } } static final class AddEdit extends PathEdit { private final ObjectId data; private final long length; public AddEdit(String entryPath, ObjectId data, long length) { super(entryPath); this.data = data; this.length = length; } @Override public void apply(DirCacheEntry ent) { ent.setFileMode(FileMode.REGULAR_FILE); ent.setLength(length); ent.setObjectId(data); } } private ObjectId buildTree(HashMap headEntries) throws IOException { DirCache lockDirCache = DirCache.newInCore(); // assertTrue(lockDirCache.lock()); DirCacheEditor editor = lockDirCache.editor(); if (headEntries != null) { for (java.util.Map.Entry e : headEntries.entrySet()) { AddEdit addEdit = new AddEdit(e.getKey(), genSha1(e.getValue()), e.getValue().length()); editor.add(addEdit); } } editor.finish(); return lockDirCache.writeTree(db.newObjectInserter()); } ObjectId genSha1(String data) { try (ObjectInserter w = db.newObjectInserter()) { ObjectId id = w.insert(Constants.OBJ_BLOB, data.getBytes(UTF_8)); w.flush(); return id; } catch (IOException e) { fail(e.toString()); } return null; } protected void go() throws IllegalStateException, IOException { prescanTwoTrees(theHead, theMerge); } @Test public void testRules4thru13_IndexEntryNotInHead() throws IOException { // rules 4 and 5 HashMap idxMap; idxMap = new HashMap<>(); idxMap.put("foo", "foo"); setupCase(null, null, idxMap); go(); assertTrue(getUpdated().isEmpty()); assertTrue(getRemoved().isEmpty()); assertTrue(getConflicts().isEmpty()); // rules 6 and 7 idxMap = new HashMap<>(); idxMap.put("foo", "foo"); setupCase(null, idxMap, idxMap); go(); assertAllEmpty(); // rules 8 and 9 HashMap mergeMap; mergeMap = new HashMap<>(); mergeMap.put("foo", "merge"); setupCase(null, mergeMap, idxMap); go(); assertTrue(getUpdated().isEmpty()); assertTrue(getRemoved().isEmpty()); assertTrue(getConflicts().contains("foo")); // rule 10 HashMap headMap = new HashMap<>(); headMap.put("foo", "foo"); setupCase(headMap, null, idxMap); go(); assertTrue(getRemoved().contains("foo")); assertTrue(getUpdated().isEmpty()); assertTrue(getConflicts().isEmpty()); // rule 11 setupCase(headMap, null, idxMap); assertTrue(new File(trash, "foo").delete()); writeTrashFile("foo", "bar"); db.readDirCache().getEntry(0).setUpdateNeeded(true); go(); assertTrue(getRemoved().isEmpty()); assertTrue(getUpdated().isEmpty()); assertTrue(getConflicts().contains("foo")); // rule 12 & 13 headMap.put("foo", "head"); setupCase(headMap, null, idxMap); go(); assertTrue(getRemoved().isEmpty()); assertTrue(getUpdated().isEmpty()); assertTrue(getConflicts().contains("foo")); // rules 14 & 15 setupCase(headMap, headMap, idxMap); go(); assertAllEmpty(); // rules 16 & 17 setupCase(headMap, mergeMap, idxMap); go(); assertTrue(getConflicts().contains("foo")); // rules 18 & 19 setupCase(headMap, idxMap, idxMap); go(); assertAllEmpty(); // rule 20 setupCase(idxMap, mergeMap, idxMap); go(); assertTrue(getUpdated().containsKey("foo")); // rules 21 setupCase(idxMap, mergeMap, idxMap); assertTrue(new File(trash, "foo").delete()); writeTrashFile("foo", "bar"); db.readDirCache().getEntry(0).setUpdateNeeded(true); go(); assertTrue(getConflicts().contains("foo")); } private void assertAllEmpty() { assertTrue(getRemoved().isEmpty()); assertTrue(getUpdated().isEmpty()); assertTrue(getConflicts().isEmpty()); } /*- * Directory/File Conflict cases: * It's entirely possible that in practice a number of these may be equivalent * to the cases described in git-read-tree.txt. As long as it does the right thing, * that's all I care about. These are basically reverse-engineered from * what git currently does. If there are tests for these in git, it's kind of * hard to track them all down... * * H I M Clean H==M H==I I==M Result * ------------------------------------------------------------------ *1 D D F Y N Y N Update *2 D D F N N Y N Conflict *3 D F D Y N N Keep *4 D F D N N N Conflict *5 D F F Y N N Y Keep *5b D F F Y N N N Conflict *6 D F F N N N Y Keep *6b D F F N N N N Conflict *7 F D F Y Y N N Update *8 F D F N Y N N Conflict *9 F D F Y N N N Update *10 F D D N N Y Keep *11 F D D N N N Conflict *12 F F D Y N Y N Update *13 F F D N N Y N Conflict *14 F F D N N N Conflict *15 0 F D N N N Conflict *16 0 D F Y N N N Update *17 0 D F N N N Conflict *18 F 0 D Update *19 D 0 F Update */ @Test public void testDirectoryFileSimple() throws IOException { ObjectId treeDF = buildTree(mkmap("DF", "DF")); ObjectId treeDFDF = buildTree(mkmap("DF/DF", "DF/DF")); buildIndex(mkmap("DF", "DF")); prescanTwoTrees(treeDF, treeDFDF); assertTrue(getRemoved().contains("DF")); assertTrue(getUpdated().containsKey("DF/DF")); recursiveDelete(new File(trash, "DF")); buildIndex(mkmap("DF/DF", "DF/DF")); prescanTwoTrees(treeDFDF, treeDF); assertTrue(getRemoved().contains("DF/DF")); assertTrue(getUpdated().containsKey("DF")); } @Test public void testDirectoryFileConflicts_1() throws Exception { // 1 doit(mk("DF/DF"), mk("DF"), mk("DF/DF")); assertNoConflicts(); assertUpdated("DF"); assertRemoved("DF/DF"); } @Test public void testDirectoryFileConflicts_2() throws Exception { // 2 setupCase(mk("DF/DF"), mk("DF"), mk("DF/DF")); writeTrashFile("DF/DF", "different"); go(); assertConflict("DF/DF"); } @Test public void testDirectoryFileConflicts_3() throws Exception { // 3 doit(mk("DF/DF"), mk("DF/DF"), mk("DF")); assertNoConflicts(); } @Test public void testDirectoryFileConflicts_4() throws Exception { // 4 (basically same as 3, just with H and M different) doit(mk("DF/DF"), mkmap("DF/DF", "foo"), mk("DF")); assertConflict("DF/DF"); } @Test public void testDirectoryFileConflicts_5() throws Exception { // 5 doit(mk("DF/DF"), mk("DF"), mk("DF")); assertRemoved("DF/DF"); assertEquals(0, dco.getConflicts().size()); assertEquals(0, dco.getUpdated().size()); } @Test public void testDirectoryFileConflicts_5b() throws Exception { // 5 doit(mk("DF/DF"), mkmap("DF", "different"), mk("DF")); assertRemoved("DF/DF"); assertConflict("DF"); assertEquals(0, dco.getUpdated().size()); } @Test public void testDirectoryFileConflicts_6() throws Exception { // 6 setupCase(mk("DF/DF"), mk("DF"), mk("DF")); writeTrashFile("DF", "different"); go(); assertRemoved("DF/DF"); assertEquals(0, dco.getConflicts().size()); assertEquals(0, dco.getUpdated().size()); } @Test public void testDirectoryFileConflicts_6b() throws Exception { // 6 setupCase(mk("DF/DF"), mk("DF"), mkmap("DF", "different")); writeTrashFile("DF", "again different"); go(); assertRemoved("DF/DF"); assertConflict("DF"); assertEquals(0, dco.getUpdated().size()); } @Test public void testDirectoryFileConflicts_7() throws Exception { // 7 doit(mk("DF"), mk("DF"), mk("DF/DF")); assertUpdated("DF"); assertRemoved("DF/DF"); cleanUpDF(); setupCase(mk("DF/DF"), mk("DF/DF"), mk("DF/DF/DF/DF/DF")); go(); assertRemoved("DF/DF/DF/DF/DF"); assertUpdated("DF/DF"); cleanUpDF(); setupCase(mk("DF/DF"), mk("DF/DF"), mk("DF/DF/DF/DF/DF")); writeTrashFile("DF/DF/DF/DF/DF", "diff"); go(); assertConflict("DF/DF/DF/DF/DF"); // assertUpdated("DF/DF"); // Why do we expect an update on DF/DF. H==M, // H&M are files and index contains a dir, index // is dirty: that case is not in the table but // we cannot update DF/DF to a file, this would // require that we delete DF/DF/DF/DF/DF in workdir // throwing away unsaved contents. // This test would fail in DirCacheCheckoutTests. } @Test public void testDirectoryFileConflicts_8() throws Exception { // 8 setupCase(mk("DF"), mk("DF"), mk("DF/DF")); recursiveDelete(new File(db.getWorkTree(), "DF")); writeTrashFile("DF", "xy"); go(); assertConflict("DF/DF"); } @Test public void testDirectoryFileConflicts_9() throws Exception { // 9 doit(mkmap("DF", "QP"), mkmap("DF", "QP"), mkmap("DF/DF", "DF/DF")); assertRemoved("DF/DF"); assertUpdated("DF"); } @Test public void testDirectoryFileConflicts_10() throws Exception { // 10 cleanUpDF(); doit(mk("DF"), mk("DF/DF"), mk("DF/DF")); assertNoConflicts(); } @Test public void testDirectoryFileConflicts_11() throws Exception { // 11 doit(mk("DF"), mk("DF/DF"), mkmap("DF/DF", "asdf")); assertConflict("DF/DF"); } @Test public void testDirectoryFileConflicts_12() throws Exception { // 12 cleanUpDF(); doit(mk("DF"), mk("DF/DF"), mk("DF")); assertRemoved("DF"); assertUpdated("DF/DF"); } @Test public void testDirectoryFileConflicts_13() throws Exception { // 13 cleanUpDF(); setupCase(mk("DF"), mk("DF/DF"), mk("DF")); writeTrashFile("DF", "asdfsdf"); go(); assertConflict("DF"); assertUpdated("DF/DF"); } @Test public void testDirectoryFileConflicts_14() throws Exception { // 14 cleanUpDF(); doit(mk("DF"), mk("DF/DF"), mkmap("DF", "Foo")); assertConflict("DF"); assertUpdated("DF/DF"); } @Test public void testDirectoryFileConflicts_15() throws Exception { // 15 doit(mkmap(), mk("DF/DF"), mk("DF")); // This test would fail in DirCacheCheckoutTests. I think this test is wrong, // it should check for conflicts according to rule 15 // assertRemoved("DF"); assertUpdated("DF/DF"); } @Test public void testDirectoryFileConflicts_15b() throws Exception { // 15, take 2, just to check multi-leveled doit(mkmap(), mk("DF/DF/DF/DF"), mk("DF")); // I think this test is wrong, it should // check for conflicts according to rule 15 // This test would fail in DirCacheCheckouts // assertRemoved("DF"); assertUpdated("DF/DF/DF/DF"); } @Test public void testDirectoryFileConflicts_16() throws Exception { // 16 cleanUpDF(); doit(mkmap(), mk("DF"), mk("DF/DF/DF")); assertRemoved("DF/DF/DF"); assertUpdated("DF"); } @Test public void testDirectoryFileConflicts_17() throws Exception { // 17 cleanUpDF(); setupCase(mkmap(), mk("DF"), mk("DF/DF/DF")); writeTrashFile("DF/DF/DF", "asdf"); go(); assertConflict("DF/DF/DF"); // Why do we expect an update on DF. If we really update // DF and update also the working tree we would have to // overwrite a dirty file in the work-tree DF/DF/DF // This test would fail in DirCacheCheckout // assertUpdated("DF"); } @Test public void testDirectoryFileConflicts_18() throws Exception { // 18 cleanUpDF(); doit(mk("DF/DF"), mk("DF/DF/DF/DF"), null); assertRemoved("DF/DF"); assertUpdated("DF/DF/DF/DF"); } @Test public void testDirectoryFileConflicts_19() throws Exception { // 19 cleanUpDF(); doit(mk("DF/DF/DF/DF"), mk("DF/DF/DF"), null); assertRemoved("DF/DF/DF/DF"); assertUpdated("DF/DF/DF"); } protected void cleanUpDF() throws Exception { tearDown(); setUp(); recursiveDelete(new File(trash, "DF")); } protected void assertConflict(String s) { assertTrue(getConflicts().contains(s)); } protected void assertUpdated(String s) { assertTrue(getUpdated().containsKey(s)); } protected void assertRemoved(String s) { assertTrue(getRemoved().contains(s)); } protected void assertNoConflicts() { assertTrue(getConflicts().isEmpty()); } protected void doit(HashMap h, HashMap m, HashMap i) throws IOException { setupCase(h, m, i); go(); } @Test public void testUntrackedConflicts() throws IOException { setupCase(null, mk("foo"), null); writeTrashFile("foo", "foo"); go(); // test that we don't overwrite untracked files when there is a HEAD recursiveDelete(new File(trash, "foo")); setupCase(mk("other"), mkmap("other", "other", "foo", "foo"), mk("other")); writeTrashFile("foo", "bar"); try { checkout(); 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")); } // test that we don't overwrite untracked files when there is no HEAD recursiveDelete(new File(trash, "other")); recursiveDelete(new File(trash, "foo")); setupCase(null, mk("foo"), null); writeTrashFile("foo", "bar"); try { checkout(); fail("didn't get the expected exception"); } catch (CheckoutConflictException e) { assertConflict("foo"); assertWorkDir(mkmap("foo", "bar")); assertIndex(mkmap("other", "other")); } // TODO: Why should we expect conflicts here? // H and M are empty and according to rule #5 of // the carry-over rules a dirty index is no reason // for a conflict. (I also feel it should be a // conflict because we are going to overwrite // unsaved content in the working tree // This test would fail in DirCacheCheckoutTest // assertConflict("foo"); recursiveDelete(new File(trash, "foo")); recursiveDelete(new File(trash, "other")); setupCase(null, mk("foo"), null); writeTrashFile("foo/bar/baz", ""); writeTrashFile("foo/blahblah", ""); go(); assertConflict("foo"); assertConflict("foo/bar/baz"); assertConflict("foo/blahblah"); recursiveDelete(new File(trash, "foo")); setupCase(mkmap("foo/bar", "", "foo/baz", ""), mk("foo"), mkmap("foo/bar", "", "foo/baz", "")); assertTrue(new File(trash, "foo/bar").exists()); go(); assertNoConflicts(); } @Test public void testCloseNameConflictsX0() throws IOException { setupCase(mkmap("a/a", "a/a-c"), mkmap("a/a","a/a", "b.b/b.b","b.b/b.bs"), mkmap("a/a", "a/a-c") ); checkout(); assertIndex(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs")); assertWorkDir(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs")); go(); assertIndex(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs")); assertWorkDir(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs")); assertNoConflicts(); } @Test public void testCloseNameConflicts1() throws IOException { setupCase(mkmap("a/a", "a/a-c"), mkmap("a/a","a/a", "a.a/a.a","a.a/a.a"), mkmap("a/a", "a/a-c") ); checkout(); assertIndex(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a")); assertWorkDir(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a")); go(); assertIndex(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a")); assertWorkDir(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a")); assertNoConflicts(); } @Test public void testCheckoutHierarchy() throws IOException { setupCase( mkmap("a", "a", "b/c", "b/c", "d", "d", "e/f", "e/f", "e/g", "e/g"), mkmap("a", "a2", "b/c", "b/c", "d", "d", "e/f", "e/f", "e/g", "e/g2"), mkmap("a", "a", "b/c", "b/c", "d", "d", "e/f", "e/f", "e/g", "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]); } } @Test public void testCheckoutOutChanges() throws IOException { setupCase(mk("foo"), mk("foo/bar"), mk("foo")); checkout(); assertIndex(mk("foo/bar")); assertWorkDir(mk("foo/bar")); assertFalse(new File(trash, "foo").isFile()); assertTrue(new File(trash, "foo/bar").isFile()); recursiveDelete(new File(trash, "foo")); assertWorkDir(mkmap()); setupCase(mk("foo/bar"), mk("foo"), mk("foo/bar")); checkout(); assertIndex(mk("foo")); assertWorkDir(mk("foo")); assertFalse(new File(trash, "foo/bar").isFile()); assertTrue(new File(trash, "foo").isFile()); setupCase(mk("foo"), mkmap("foo", "qux"), mkmap("foo", "bar")); assertIndex(mkmap("foo", "bar")); assertWorkDir(mkmap("foo", "bar")); try { checkout(); fail("did not throw exception"); } catch (CheckoutConflictException e) { assertIndex(mkmap("foo", "bar")); assertWorkDir(mkmap("foo", "bar")); } } @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(); assertIndex(mkmap("foo/bar", "foo\nbar")); assertWorkDir(mkmap("foo/bar", "foo\nbar")); } @Test public void testCheckoutOutChangesAutoCRLFInput() throws IOException { setupCase(mk("foo"), mkmap("foo/bar", "foo\nbar"), mk("foo")); db.getConfig().setString("core", null, "autocrlf", "input"); checkout(); assertIndex(mkmap("foo/bar", "foo\nbar")); assertWorkDir(mkmap("foo/bar", "foo\nbar")); } @Test public void testCheckoutOutChangesAutoCRLFtrue() throws IOException { setupCase(mk("foo"), mkmap("foo/bar", "foo\nbar"), mk("foo")); db.getConfig().setString("core", null, "autocrlf", "true"); checkout(); assertIndex(mkmap("foo/bar", "foo\nbar")); assertWorkDir(mkmap("foo/bar", "foo\r\nbar")); } @Test public void testCheckoutOutChangesAutoCRLFtrueBinary() throws IOException { setupCase(mk("foo"), mkmap("foo/bar", "foo\nb\u0000ar"), mk("foo")); db.getConfig().setString("core", null, "autocrlf", "true"); checkout(); assertIndex(mkmap("foo/bar", "foo\nb\u0000ar")); assertWorkDir(mkmap("foo/bar", "foo\nb\u0000ar")); } @Test public void testCheckoutUncachedChanges() throws IOException { setupCase(mk("foo"), mk("foo"), mk("foo")); writeTrashFile("foo", "otherData"); checkout(); assertIndex(mk("foo")); assertWorkDir(mkmap("foo", "otherData")); assertTrue(new File(trash, "foo").isFile()); } @Test public void testDontOverwriteDirtyFile() throws IOException { setupCase(mk("foo"), mk("other"), mk("foo")); writeTrashFile("foo", "different"); try { checkout(); fail("Didn't got the expected conflict"); } catch (CheckoutConflictException e) { assertIndex(mk("foo")); assertWorkDir(mkmap("foo", "different")); assertEquals(Arrays.asList("foo"), getConflicts()); assertTrue(new File(trash, "foo").isFile()); } } @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"; 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(); } } } @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(); } } } @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 public void testFileModeChangeWithNoContentChangeUpdate() throws Exception { if (!FS.DETECTED.supportsExecute()) return; 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 public void testFileModeChangeAndContentChangeConflict() throws Exception { if (!FS.DETECTED.supportsExecute()) return; 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(); } } } @Test public void testDirtyFileModeEqualHeadMerge() throws Exception { if (!FS.DETECTED.supportsExecute()) return; 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(); } } } @Test public void testDirtyFileModeEqualIndexMerge() throws Exception { if (!FS.DETECTED.supportsExecute()) return; 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(); } } } @Test public void testFileModeChangeAndContentChangeNoConflict() throws Exception { if (!FS.DETECTED.supportsExecute()) return; 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(); } } } @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 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"); 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")); } } @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"); 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")); } } @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); } @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)); } } @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"); } } @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)); } } @Test public void testCheckoutWithEmptyIndexDoesntOverwrite() throws Exception { try (Git git = new Git(db); TestRepository db_t = new TestRepository<>(db)) { // 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")); } } private static class TestFileTreeIterator extends FileTreeIterator { // For assertions only private final int[] count; public TestFileTreeIterator(Repository repo, int[] count) { super(repo); this.count = count; } protected TestFileTreeIterator(final WorkingTreeIterator p, final File root, FS fs, FileModeStrategy fileModeStrategy, int[] count) { super(p, root, fs, fileModeStrategy); this.count = count; } @Override protected AbstractTreeIterator enterSubtree() { count[0] += 1; return new TestFileTreeIterator(this, ((FileEntry) current()).getFile(), fs, fileModeStrategy, count); } } 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(Map i) throws CorruptObjectException, IOException { 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(); } } assertEquals("WorkDir has not the right size.", i.size(), nrFiles); } } }