Merge "Fix off-by-one error in Strings.count()"
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
index b2099ae..8051080 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/mbarbero/fr.obeo.releng.targetplatform -->
-<target name="jgit-4.5" sequenceNumber="1496008880">
+<target name="jgit-4.5" sequenceNumber="1502749391">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.jetty.client" version="9.4.5.v20170502"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
index e4baa5d..b6bbcda 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/mbarbero/fr.obeo.releng.targetplatform -->
-<target name="jgit-4.6" sequenceNumber="1496008884">
+<target name="jgit-4.6" sequenceNumber="1502749371">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.jetty.client" version="9.4.5.v20170502"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
index 9a15741..6071c8f 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/mbarbero/fr.obeo.releng.targetplatform -->
-<target name="jgit-4.7" sequenceNumber="1496008862">
+<target name="jgit-4.7" sequenceNumber="1502749365">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.jetty.client" version="9.4.5.v20170502"/>
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 8666c34..7c9816c 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -257,7 +257,7 @@
 usage_ReadDirCache= Read the DirCache 100 times
 usage_RebuildCommitGraph=Recreate a repository from another one's commit graph
 usage_RebuildRefTree=Copy references into a RefTree
-usage_RebuildRefTreeEnable=set extensions.refsStorage = reftree
+usage_RebuildRefTreeEnable=set extensions.refStorage = reftree
 usage_Remote=Manage set of tracked repositories
 usage_RepositoryToReadFrom=Repository to read from
 usage_RepositoryToReceiveInto=Repository to receive into
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
index 57345e2..8cde513 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
@@ -133,7 +133,7 @@
 			if (enable && !(db.getRefDatabase() instanceof RefTreeDatabase)) {
 				StoredConfig cfg = db.getConfig();
 				cfg.setInt("core", null, "repositoryformatversion", 1); //$NON-NLS-1$ //$NON-NLS-2$
-				cfg.setString("extensions", null, "refsStorage", "reftree"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+				cfg.setString("extensions", null, "refStorage", "reftree"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 				cfg.save();
 				errw.println("Enabled reftree."); //$NON-NLS-1$
 				errw.flush();
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index dad1e3c..084014c 100644
--- a/org.eclipse.jgit.test/pom.xml
+++ b/org.eclipse.jgit.test/pom.xml
@@ -66,7 +66,6 @@
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
-      <scope>test</scope>
     </dependency>
 
     <!-- Optional security provider for encryption tests. -->
diff --git a/org.eclipse.jgit.test/src/org/eclipse/jgit/events/ChangeRecorder.java b/org.eclipse.jgit.test/src/org/eclipse/jgit/events/ChangeRecorder.java
new file mode 100644
index 0000000..c5582a8
--- /dev/null
+++ b/org.eclipse.jgit.test/src/org/eclipse/jgit/events/ChangeRecorder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.events;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A {@link WorkingTreeModifiedListener} that can be used in tests to check
+ * expected events.
+ */
+public class ChangeRecorder implements WorkingTreeModifiedListener {
+
+	public static final String[] EMPTY = new String[0];
+
+	private Set<String> modified = new HashSet<>();
+
+	private Set<String> deleted = new HashSet<>();
+
+	private int eventCount;
+
+	@Override
+	public void onWorkingTreeModified(WorkingTreeModifiedEvent event) {
+		eventCount++;
+		modified.removeAll(event.getDeleted());
+		deleted.removeAll(event.getModified());
+		modified.addAll(event.getModified());
+		deleted.addAll(event.getDeleted());
+	}
+
+	private String[] getModified() {
+		return modified.toArray(new String[modified.size()]);
+	}
+
+	private String[] getDeleted() {
+		return deleted.toArray(new String[deleted.size()]);
+	}
+
+	private void reset() {
+		eventCount = 0;
+		modified.clear();
+		deleted.clear();
+	}
+
+	public void assertNoEvent() {
+		assertEquals("Unexpected WorkingTreeModifiedEvent ", 0, eventCount);
+	}
+
+	public void assertEvent(String[] expectedModified,
+			String[] expectedDeleted) {
+		String[] actuallyModified = getModified();
+		String[] actuallyDeleted = getDeleted();
+		Arrays.sort(actuallyModified);
+		Arrays.sort(expectedModified);
+		Arrays.sort(actuallyDeleted);
+		Arrays.sort(expectedDeleted);
+		assertArrayEquals("Unexpected modifications reported", expectedModified,
+				actuallyModified);
+		assertArrayEquals("Unexpected deletions reported", expectedDeleted,
+				actuallyDeleted);
+		reset();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
index ed3907e..aafda01 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
@@ -303,6 +303,21 @@
 	}
 
 	@Test
+	public void testAttributesConflictingMatch() throws Exception {
+		writeTrashFile(".gitattributes", "foo/** crlf=input\n*.jar binary");
+		writeTrashFile("foo/bar.jar", "\r\n");
+		// We end up with attributes [binary -diff -merge -text crlf=input].
+		// crlf should have no effect when -text is present.
+		try (Git git = new Git(db)) {
+			git.add().addFilepattern(".").call();
+			assertEquals(
+					"[.gitattributes, mode:100644, content:foo/** crlf=input\n*.jar binary]"
+							+ "[foo/bar.jar, mode:100644, content:\r\n]",
+					indexState(CONTENT));
+		}
+	}
+
+	@Test
 	public void testCleanFilterEnvironment()
 			throws IOException, GitAPIException {
 		writeTrashFile(".gitattributes", "*.txt filter=tstFilter");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
index f2e4d5b..ad3ab7f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
@@ -55,12 +55,15 @@
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.api.errors.StashApplyFailureException;
+import org.eclipse.jgit.events.ChangeRecorder;
+import org.eclipse.jgit.events.ListenerHandle;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.util.FileUtils;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -77,15 +80,31 @@
 
 	private File committedFile;
 
+	private ChangeRecorder recorder;
+
+	private ListenerHandle handle;
+
 	@Override
 	@Before
 	public void setUp() throws Exception {
 		super.setUp();
 		git = Git.wrap(db);
+		recorder = new ChangeRecorder();
+		handle = db.getListenerList().addWorkingTreeModifiedListener(recorder);
 		committedFile = writeTrashFile(PATH, "content");
 		git.add().addFilepattern(PATH).call();
 		head = git.commit().setMessage("add file").call();
 		assertNotNull(head);
+		recorder.assertNoEvent();
+	}
+
+	@Override
+	@After
+	public void tearDown() throws Exception {
+		if (handle != null) {
+			handle.remove();
+		}
+		super.tearDown();
 	}
 
 	@Test
@@ -95,10 +114,12 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertFalse(committedFile.exists());
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { PATH });
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -121,11 +142,13 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertFalse(addedFile.exists());
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { addedPath });
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertTrue(addedFile.exists());
 		assertEquals("content2", read(addedFile));
+		recorder.assertEvent(new String[] { addedPath }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getChanged().isEmpty());
@@ -142,14 +165,17 @@
 	@Test
 	public void indexDelete() throws Exception {
 		git.rm().addFilepattern("file.txt").call();
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "file.txt" });
 
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
+		recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY);
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertFalse(committedFile.exists());
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "file.txt" });
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -170,10 +196,12 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
+		recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY);
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertEquals("content2", read(committedFile));
+		recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -193,16 +221,21 @@
 		File subfolderFile = writeTrashFile(path, "content");
 		git.add().addFilepattern(path).call();
 		head = git.commit().setMessage("add file").call();
+		recorder.assertNoEvent();
 
 		writeTrashFile(path, "content2");
 
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertEquals("content", read(subfolderFile));
+		recorder.assertEvent(new String[] { "d1/d2/f.txt" },
+				ChangeRecorder.EMPTY);
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertEquals("content2", read(subfolderFile));
+		recorder.assertEvent(new String[] { "d1/d2/f.txt", "d1/d2", "d1" },
+				ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -225,10 +258,12 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
+		recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY);
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertEquals("content3", read(committedFile));
+		recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -252,10 +287,12 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
+		recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY);
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertEquals("content2", read(committedFile));
+		recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -281,10 +318,12 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertFalse(added.exists());
+		recorder.assertNoEvent();
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertEquals("content2", read(added));
+		recorder.assertEvent(new String[] { path }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getChanged().isEmpty());
@@ -308,10 +347,12 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
 		assertFalse(committedFile.exists());
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { PATH });
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -337,9 +378,13 @@
 		assertNotNull(stashed);
 		assertTrue(committedFile.exists());
 		assertFalse(addedFile.exists());
+		recorder.assertEvent(new String[] { PATH },
+				new String[] { "file2.txt" });
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
+		recorder.assertEvent(new String[] { "file2.txt" },
+				new String[] { PATH });
 
 		Status status = git.status().call();
 		assertTrue(status.getChanged().isEmpty());
@@ -362,6 +407,7 @@
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		writeTrashFile(PATH, "content3");
 
@@ -372,6 +418,7 @@
 			// expected
  		}
 		assertEquals("content3", read(PATH));
+		recorder.assertNoEvent();
 	}
 
 	@Test
@@ -391,10 +438,12 @@
 		assertEquals("content\nhead change\nmore content\n",
 				read(committedFile));
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		writeTrashFile(PATH, "content\nmore content\ncommitted change\n");
 		git.add().addFilepattern(PATH).call();
 		git.commit().setMessage("committed change").call();
+		recorder.assertNoEvent();
 
 		try {
 			git.stashApply().call();
@@ -402,6 +451,7 @@
 		} catch (StashApplyFailureException e) {
 			// expected
 		}
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 		Status status = new StatusCommand(db).call();
 		assertEquals(1, status.getConflicting().size());
 		assertEquals(
@@ -426,12 +476,15 @@
 		writeTrashFile(PATH, "master content");
 		git.add().addFilepattern(PATH).call();
 		git.commit().setMessage("even content").call();
+		recorder.assertNoEvent();
 
 		git.checkout().setName(otherBranch).call();
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		writeTrashFile(PATH, "otherBranch content");
 		git.add().addFilepattern(PATH).call();
 		git.commit().setMessage("even more content").call();
+		recorder.assertNoEvent();
 
 		writeTrashFile(path2, "content\nstashed change\nmore content\n");
 
@@ -442,12 +495,15 @@
 		assertEquals("otherBranch content",
 				read(committedFile));
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY);
 
 		git.checkout().setName("master").call();
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 		git.stashApply().call();
 		assertEquals("content\nstashed change\nmore content\n", read(file2));
 		assertEquals("master content",
 				read(committedFile));
+		recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY);
 	}
 
 	@Test
@@ -467,12 +523,15 @@
 		writeTrashFile(PATH, "master content");
 		git.add().addFilepattern(PATH).call();
 		git.commit().setMessage("even content").call();
+		recorder.assertNoEvent();
 
 		git.checkout().setName(otherBranch).call();
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		writeTrashFile(PATH, "otherBranch content");
 		git.add().addFilepattern(PATH).call();
 		git.commit().setMessage("even more content").call();
+		recorder.assertNoEvent();
 
 		writeTrashFile(path2,
 				"content\nstashed change in index\nmore content\n");
@@ -485,8 +544,10 @@
 		assertEquals("content\nmore content\n", read(file2));
 		assertEquals("otherBranch content", read(committedFile));
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY);
 
 		git.checkout().setName("master").call();
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 		git.stashApply().call();
 		assertEquals("content\nstashed change\nmore content\n", read(file2));
 		assertEquals(
@@ -494,6 +555,7 @@
 						+ "[file2.txt, mode:100644, content:content\nstashed change in index\nmore content\n]",
 				indexState(CONTENT));
 		assertEquals("master content", read(committedFile));
+		recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY);
 	}
 
 	@Test
@@ -501,6 +563,7 @@
 		writeTrashFile(PATH, "content\nmore content\n");
 		git.add().addFilepattern(PATH).call();
 		git.commit().setMessage("more content").call();
+		recorder.assertNoEvent();
 
 		writeTrashFile(PATH, "content\nstashed change\nmore content\n");
 
@@ -508,15 +571,18 @@
 		assertNotNull(stashed);
 		assertEquals("content\nmore content\n", read(committedFile));
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		writeTrashFile(PATH, "content\nmore content\ncommitted change\n");
 		git.add().addFilepattern(PATH).call();
 		git.commit().setMessage("committed change").call();
+		recorder.assertNoEvent();
 
 		git.stashApply().call();
 		assertEquals(
 				"content\nstashed change\nmore content\ncommitted change\n",
 				read(committedFile));
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 	}
 
 	@Test
@@ -527,6 +593,7 @@
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		writeTrashFile(PATH, "content3");
 		git.add().addFilepattern(PATH).call();
@@ -538,6 +605,7 @@
 		} catch (StashApplyFailureException e) {
 			// expected
 		}
+		recorder.assertNoEvent();
 		assertEquals("content2", read(PATH));
 	}
 
@@ -549,6 +617,7 @@
 		assertNotNull(stashed);
 		assertEquals("content", read(committedFile));
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		String path2 = "file2.txt";
 		writeTrashFile(path2, "content3");
@@ -557,6 +626,7 @@
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getAdded().isEmpty());
@@ -583,12 +653,15 @@
 		RevCommit stashed = git.stashCreate().call();
 		assertNotNull(stashed);
 		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(ChangeRecorder.EMPTY,
+				new String[] { subdir, path });
 
 		git.branchCreate().setName(otherBranch).call();
 		git.checkout().setName(otherBranch).call();
 
 		ObjectId unstashed = git.stashApply().call();
 		assertEquals(stashed, unstashed);
+		recorder.assertEvent(new String[] { path }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertTrue(status.getChanged().isEmpty());
@@ -643,12 +716,15 @@
 		git.commit().setMessage("x").call();
 		file.delete();
 		git.rm().addFilepattern("file").call();
+		recorder.assertNoEvent();
 		git.stashCreate().call();
+		recorder.assertEvent(new String[] { "file" }, ChangeRecorder.EMPTY);
 		file.delete();
 
 		git.stashApply().setStashRef("stash@{0}").call();
 
 		assertFalse(file.exists());
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "file" });
 	}
 
 	@Test
@@ -660,9 +736,11 @@
 		git.add().addFilepattern(PATH).call();
 		git.stashCreate().call();
 		assertTrue(untrackedFile.exists());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		git.stashApply().setStashRef("stash@{0}").call();
 		assertTrue(untrackedFile.exists());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertEquals(1, status.getUntracked().size());
@@ -684,11 +762,14 @@
 				.call();
 		assertNotNull(stashedCommit);
 		assertFalse(untrackedFile.exists());
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { path });
+
 		deleteTrashFile("a/b"); // checkout should create parent dirs
 
 		git.stashApply().setStashRef("stash@{0}").call();
 		assertTrue(untrackedFile.exists());
 		assertEquals("content", read(path));
+		recorder.assertEvent(new String[] { path }, ChangeRecorder.EMPTY);
 
 		Status status = git.status().call();
 		assertEquals(1, status.getUntracked().size());
@@ -706,6 +787,7 @@
 		String path = "untracked.txt";
 		writeTrashFile(path, "untracked");
 		git.stashCreate().setIncludeUntracked(true).call();
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { path });
 
 		writeTrashFile(path, "committed");
 		head = git.commit().setMessage("add file").call();
@@ -719,6 +801,7 @@
 			assertEquals(e.getMessage(), JGitText.get().stashApplyConflict);
 		}
 		assertEquals("committed", read(path));
+		recorder.assertNoEvent();
 	}
 
 	@Test
@@ -727,6 +810,7 @@
 		String path = "untracked.txt";
 		writeTrashFile(path, "untracked");
 		git.stashCreate().setIncludeUntracked(true).call();
+		recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { path });
 
 		writeTrashFile(path, "working-directory");
 		try {
@@ -736,6 +820,7 @@
 			assertEquals(e.getMessage(), JGitText.get().stashApplyConflict);
 		}
 		assertEquals("working-directory", read(path));
+		recorder.assertNoEvent();
 	}
 
 	@Test
@@ -747,11 +832,13 @@
 		assertTrue(PATH + " should exist", check(PATH));
 		assertEquals(PATH + " should have been reset", "content", read(PATH));
 		assertFalse(path + " should not exist", check(path));
+		recorder.assertEvent(new String[] { PATH }, new String[] { path });
 		git.stashApply().setStashRef("stash@{0}").call();
 		assertTrue(PATH + " should exist", check(PATH));
 		assertEquals(PATH + " should have new content", "changed", read(PATH));
 		assertTrue(path + " should exist", check(path));
 		assertEquals(path + " should have new content", "untracked",
 				read(path));
+		recorder.assertEvent(new String[] { PATH, path }, ChangeRecorder.EMPTY);
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
index ebb5a4f..643bb49 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
@@ -45,8 +45,12 @@
 
 import static java.lang.Integer.valueOf;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CyclicBarrier;
@@ -56,8 +60,14 @@
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackWriter;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.EmptyProgressMonitor;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Sets;
 import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class GcConcurrentTest extends GcTestCase {
@@ -118,4 +128,97 @@
 			pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
 		}
 	}
+
+	@Test
+	public void repackAndGetStats() throws Exception {
+		TestRepository<FileRepository>.BranchBuilder test = tr.branch("test");
+		test.commit().add("a", "a").create();
+		GC gc1 = new GC(tr.getRepository());
+		gc1.setPackExpireAgeMillis(0);
+		gc1.gc();
+		test.commit().add("b", "b").create();
+
+		// Create a new Repository instance and trigger a gc
+		// from that instance. Reusing the existing repo instance
+		// tr.getRepository() would not show the problem.
+		FileRepository r2 = new FileRepository(
+				tr.getRepository().getDirectory());
+		GC gc2 = new GC(r2);
+		gc2.setPackExpireAgeMillis(0);
+		gc2.gc();
+
+		new GC(tr.getRepository()).getStatistics();
+	}
+
+	@Test
+	public void repackAndUploadPack() throws Exception {
+		TestRepository<FileRepository>.BranchBuilder test = tr.branch("test");
+		// RevCommit a = test.commit().add("a", "a").create();
+		test.commit().add("a", "a").create();
+
+		GC gc1 = new GC(tr.getRepository());
+		gc1.setPackExpireAgeMillis(0);
+		gc1.gc();
+
+		RevCommit b = test.commit().add("b", "b").create();
+
+		FileRepository r2 = new FileRepository(
+				tr.getRepository().getDirectory());
+		GC gc2 = new GC(r2);
+		gc2.setPackExpireAgeMillis(0);
+		gc2.gc();
+
+		// Simulate parts of an UploadPack. This is the situation on
+		// server side (e.g. gerrit) when when clients are
+		// cloning/fetching while the server side repo's
+		// are gc'ed by an external process (e.g. scheduled
+		// native git gc)
+		try (PackWriter pw = new PackWriter(tr.getRepository())) {
+			pw.setUseBitmaps(true);
+			pw.preparePack(NullProgressMonitor.INSTANCE, Sets.of(b),
+					Collections.<ObjectId> emptySet());
+			new GC(tr.getRepository()).getStatistics();
+		}
+	}
+
+	PackFile getSinglePack(FileRepository r) {
+		Collection<PackFile> packs = r.getObjectDatabase().getPacks();
+		assertEquals(1, packs.size());
+		return packs.iterator().next();
+	}
+
+	@Test
+	public void repackAndCheckBitmapUsage() throws Exception {
+		// create a test repository with one commit and pack all objects. After
+		// packing create loose objects to trigger creation of a new packfile on
+		// the next gc
+		TestRepository<FileRepository>.BranchBuilder test = tr.branch("test");
+		test.commit().add("a", "a").create();
+		FileRepository repository = tr.getRepository();
+		GC gc1 = new GC(repository);
+		gc1.setPackExpireAgeMillis(0);
+		gc1.gc();
+		String oldPackName = getSinglePack(repository).getPackName();
+		RevCommit b = test.commit().add("b", "b").create();
+
+		// start the garbage collection on a new repository instance,
+		FileRepository repository2 = new FileRepository(repository.getDirectory());
+		GC gc2 = new GC(repository2);
+		gc2.setPackExpireAgeMillis(0);
+		gc2.gc();
+		String newPackName = getSinglePack(repository2).getPackName();
+		// make sure gc() has caused creation of a new packfile
+		assertNotEquals(oldPackName, newPackName);
+
+		// Even when asking again for the set of packfiles outdated data
+		// will be returned. As long as the repository can work on cached data
+		// it will do so and not detect that a new packfile exists.
+		assertNotEquals(getSinglePack(repository).getPackName(), newPackName);
+
+		// Only when accessing object content it is required to rescan the pack
+		// directory and the new packfile will be detected.
+		repository.getObjectDatabase().open(b).getSize();
+		assertEquals(getSinglePack(repository).getPackName(), newPackName);
+		assertNotNull(getSinglePack(repository).getBitmapIndex());
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
index 67a7819..d5a07e0 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
@@ -83,7 +83,7 @@
 		FileRepository init = createWorkRepository();
 		FileBasedConfig cfg = init.getConfig();
 		cfg.setInt("core", null, "repositoryformatversion", 1);
-		cfg.setString("extensions", null, "refsStorage", "reftree");
+		cfg.setString("extensions", null, "refStorage", "reftree");
 		cfg.save();
 
 		repo = (FileRepository) new FileRepositoryBuilder()
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 f8c2d45..05573b9 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
@@ -72,6 +72,8 @@
 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;
@@ -141,14 +143,19 @@
 	@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()");
@@ -156,9 +163,12 @@
 			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());
@@ -167,26 +177,41 @@
 			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();
+			}
 		}
 	}
 
@@ -202,13 +227,18 @@
 	@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");
@@ -217,6 +247,11 @@
 			// 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();
+			}
 		}
 	}
 
@@ -227,13 +262,22 @@
 	 */
 	@Test
 	public void testInitialCheckout() throws Exception {
+		ChangeRecorder recorder = new ChangeRecorder();
+		ListenerHandle handle = null;
 		try (Git git = new Git(db)) {
+			handle = db.getListenerList()
+					.addWorkingTreeModifiedListener(recorder);
 			TestRepository<Repository> db_t = new TestRepository<>(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());
+			recorder.assertEvent(new String[] { "f" }, ChangeRecorder.EMPTY);
+		} finally {
+			if (handle != null) {
+				handle.remove();
+			}
 		}
 	}
 
@@ -930,120 +974,154 @@
 	public void testCheckoutChangeLinkToEmptyDir() throws Exception {
 		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
 		String fname = "was_file";
-		Git git = Git.wrap(db);
+		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 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();
 
-		// 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"));
 
-		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());
 
-		// 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();
 
-		// modify file
-		writeTrashFile(fname, "b");
-		assertWorkDir(mkmap(fname, "b", linkName, "/"));
+			// revert both paths to HEAD state
+			git.checkout().setStartPoint(Constants.HEAD).addPath(fname)
+					.addPath(linkName).call();
 
-		// 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);
 
-		assertWorkDir(mkmap(fname, "a", linkName, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			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";
-		Git git = Git.wrap(db);
+		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 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();
 
-		// 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"));
 
-		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());
 
-		// 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());
 
-		assertFalse("Must not delete non empty directory", link.delete());
+			// modify file
+			writeTrashFile(fname, "b");
+			assertWorkDir(mkmap(fname, "b", linkName + "/dummyDir", "/"));
+			recorder.assertNoEvent();
 
-		// modify file
-		writeTrashFile(fname, "b");
-		assertWorkDir(mkmap(fname, "b", linkName + "/dummyDir", "/"));
+			// revert both paths to HEAD state
+			git.checkout().setStartPoint(Constants.HEAD).addPath(fname)
+					.addPath(linkName).call();
 
-		// 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);
 
-		assertWorkDir(mkmap(fname, "a", linkName, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			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";
-		Git git = Git.wrap(db);
+		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 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();
 
-		// 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"));
 
-		assertWorkDir(mkmap(linkName, "a", fname, "a"));
+			// replace link with directory containing only directories, no files
+			FileUtils.delete(link);
 
-		// 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 + "/dir1", "file1", "c");
+			// create but do not add a file in the new directory to the index
+			writeTrashFile(linkName + "/dir2", "file2", "d");
 
-		// 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());
 
-		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();
 
-		// 2 extra files are created
-		assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c",
-				linkName + "/dir2/file2", "d"));
+			// revert path to HEAD state
+			git.checkout().setStartPoint(Constants.HEAD).addPath(linkName)
+					.call();
 
-		// 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);
 
-		// expect only the one added to the index
-		assertWorkDir(mkmap(linkName, "a", fname, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			Status st = git.status().call();
+			assertTrue(st.isClean());
+		} finally {
+			if (handle != null) {
+				handle.remove();
+			}
+		}
 	}
 
 	@Test
@@ -1051,174 +1129,222 @@
 			throws Exception {
 		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
 		String fname = "file";
-		Git git = Git.wrap(db);
+		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 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();
 
-		// 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"));
 
-		assertWorkDir(mkmap(linkName, "a", fname, "a"));
+			// replace link with directory containing only directories, no files
+			FileUtils.delete(link);
 
-		// 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 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");
 
-		// 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());
 
-		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();
 
-		// 2 extra files are created
-		assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c",
-				linkName + "/dir2/file2", "d"));
+			// revert path to HEAD state
+			git.checkout().setStartPoint(Constants.HEAD).addPath(linkName)
+					.call();
 
-		// 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);
 
-		// original file and link
-		assertWorkDir(mkmap(linkName, "a", fname, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			Status st = git.status().call();
+			assertTrue(st.isClean());
+		} finally {
+			if (handle != null) {
+				handle.remove();
+			}
+		}
 	}
 
 	@Test
 	public void testCheckoutChangeFileToEmptyDir() throws Exception {
 		String fname = "was_file";
-		Git git = Git.wrap(db);
+		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();
 
-		// 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();
 
-		// replace file with empty directory
-		FileUtils.delete(file);
-		FileUtils.mkdir(file);
-		assertTrue("File must be a directory now", file.isDirectory());
+			// revert path to HEAD state
+			git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
+			assertWorkDir(mkmap(fname, "a"));
+			recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY);
 
-		assertWorkDir(mkmap(fname, "/"));
-
-		// revert path to HEAD state
-		git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
-
-		assertWorkDir(mkmap(fname, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			Status st = git.status().call();
+			assertTrue(st.isClean());
+		} finally {
+			if (handle != null) {
+				handle.remove();
+			}
+		}
 	}
 
 	@Test
 	public void testCheckoutChangeFileToEmptyDirs() throws Exception {
 		String fname = "was_file";
-		Git git = Git.wrap(db);
+		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();
 
-		// 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());
 
-		// 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();
 
-		assertWorkDir(mkmap(fname + "/dummyDir", "/"));
+			// revert path to HEAD state
+			git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
+			assertWorkDir(mkmap(fname, "a"));
+			recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY);
 
-		// revert path to HEAD state
-		git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
-
-		assertWorkDir(mkmap(fname, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			Status st = git.status().call();
+			assertTrue(st.isClean());
+		} finally {
+			if (handle != null) {
+				handle.remove();
+			}
+		}
 	}
 
 	@Test
 	public void testCheckoutChangeFileToNonEmptyDirs() throws Exception {
 		String fname = "was_file";
-		Git git = Git.wrap(db);
+		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();
 
-		// Add a file
-		File file = writeTrashFile(fname, "a");
-		git.add().addFilepattern(fname).call();
-		git.commit().setMessage("Added file").call();
+			assertWorkDir(mkmap(fname, "a"));
 
-		assertWorkDir(mkmap(fname, "a"));
+			// replace file with directory containing only directories, no files
+			FileUtils.delete(file);
 
-		// 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 + "/dir1", "file1", "c");
+			// create but do not add a file in the new directory to the index
+			writeTrashFile(fname + "/dir2", "file2", "d");
 
-		// 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());
 
-		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();
 
-		// 2 extra files are created
-		assertWorkDir(
-				mkmap(fname + "/dir1/file1", "c", fname + "/dir2/file2", "d"));
+			// revert path to HEAD state
+			git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
 
-		// 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);
 
-		// expect only the one added to the index
-		assertWorkDir(mkmap(fname, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			Status st = git.status().call();
+			assertTrue(st.isClean());
+		} finally {
+			if (handle != null) {
+				handle.remove();
+			}
+		}
 	}
 
 	@Test
 	public void testCheckoutChangeFileToNonEmptyDirsAndNewIndexEntry()
 			throws Exception {
 		String fname = "was_file";
-		Git git = Git.wrap(db);
+		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();
 
-		// Add a file
-		File file = writeTrashFile(fname, "a");
-		git.add().addFilepattern(fname).call();
-		git.commit().setMessage("Added file").call();
+			assertWorkDir(mkmap(fname, "a"));
 
-		assertWorkDir(mkmap(fname, "a"));
+			// replace file with directory containing only directories, no files
+			FileUtils.delete(file);
 
-		// 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 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");
 
-		// 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());
 
-		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();
 
-		// 2 extra files are created
-		assertWorkDir(
-				mkmap(fname + "/dir/file1", "c", fname + "/dir/file2", "d"));
-
-		// revert path to HEAD state
-		git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call();
-		assertWorkDir(mkmap(fname, "a"));
-
-		Status st = git.status().call();
-		assertTrue(st.isClean());
+			// 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
@@ -1293,76 +1419,100 @@
 	public void testOverwriteUntrackedIgnoredFile() throws IOException,
 			GitAPIException {
 		String fname="file.txt";
-		Git git = Git.wrap(db);
+		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();
 
-		// Add a file
-		writeTrashFile(fname, "a");
-		git.add().addFilepattern(fname).call();
-		git.commit().setMessage("create file").call();
+			// Create branch
+			git.branchCreate().setName("side").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();
 
-		// Modify file
-		writeTrashFile(fname, "b");
-		git.add().addFilepattern(fname).call();
-		git.commit().setMessage("modify file").call();
+			// 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();
 
-		// 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();
-
-		writeTrashFile(fname, "Something different");
-		git.checkout().setName("master").call();
-		assertWorkDir(mkmap(fname, "b"));
-		assertTrue(git.status().call().isClean());
+			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";
-		Git git = Git.wrap(db);
+		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"));
 
-		// 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();
 
-		// Create branch
-		git.branchCreate().setName("side").call();
+			// Switch branches
+			git.checkout().setName("side").call();
+			recorder.assertNoEvent();
 
-		// Switch branches
-		git.checkout().setName("side").call();
+			// replace file with directory containing files
+			FileUtils.delete(file);
 
-		// 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 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");
 
-		// 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());
 
-		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
+			// 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();
+			}
 		}
 	}
 
@@ -1371,50 +1521,60 @@
 			throws Exception {
 		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
 		String fname = "file.txt";
-		Git git = Git.wrap(db);
+		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 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();
 
-		// 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"));
 
-		assertWorkDir(mkmap(linkName, "a", fname, "a"));
+			// Create branch
+			git.branchCreate().setName("side").call();
 
-		// Create branch
-		git.branchCreate().setName("side").call();
+			// Switch branches
+			git.checkout().setName("side").call();
+			recorder.assertNoEvent();
 
-		// Switch branches
-		git.checkout().setName("side").call();
+			// replace link with directory containing files
+			FileUtils.delete(link);
 
-		// 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 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");
 
-		// 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());
 
-		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
+			// 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();
+			}
 		}
 	}
 
@@ -1423,36 +1583,47 @@
 		if (!FS.DETECTED.supportsExecute())
 			return;
 
-		Git git = Git.wrap(db);
+		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));
 
-		// 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 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();
 
-		// 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));
 
-		// 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();
 
-		// 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));
+			// 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
@@ -1460,41 +1631,50 @@
 		if (!FS.DETECTED.supportsExecute())
 			return;
 
-		Git git = Git.wrap(db);
+		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));
 
-		// 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 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();
 
-		// 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));
 
-		// 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");
 
-		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"));
+			// 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();
+			}
 		}
 	}
 
@@ -1504,40 +1684,52 @@
 		if (!FS.DETECTED.supportsExecute())
 			return;
 
-		Git git = Git.wrap(db);
+		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));
 
-		// 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 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();
 
-		// 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();
 
-		// 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");
 
-		// 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();
 
-		assertEquals(
-				"[file.txt, mode:100755, content:a][file2.txt, mode:100644, content:]",
-				indexState(CONTENT));
-		assertWorkDir(mkmap("file.txt", "b", "file2.txt", ""));
-
-		// 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"));
+			// 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
@@ -1546,40 +1738,53 @@
 		if (!FS.DETECTED.supportsExecute())
 			return;
 
-		Git git = Git.wrap(db);
+		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));
 
-		// 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 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();
 
-		// 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();
 
-		// 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);
 
-		// 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();
 
-		assertEquals("[file.txt, mode:100644, content:a]", indexState(CONTENT));
-		assertWorkDir(mkmap("file.txt", "c"));
-
-		// 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"));
+			// 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
@@ -1587,31 +1792,44 @@
 		if (!FS.DETECTED.supportsExecute())
 			return;
 
-		Git git = Git.wrap(db);
+		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 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();
 
-		// 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));
+			// 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" });
 
-		// Create branch from first commit
-		assertNotNull(git.checkout().setCreateBranch(true).setName("b1")
-				.setStartPoint(Constants.HEAD + "~1").call());
+			// 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();
 
-		// 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());
+			// 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)
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java
index 0111b94..d89aabe 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java
@@ -43,11 +43,13 @@
 
 package org.eclipse.jgit.lib;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Set;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -118,6 +120,31 @@
 		assertTrue(indexDiff.diff());
 	}
 
+	private void assertDiff(IndexDiff indexDiff, IgnoreSubmoduleMode mode,
+			IgnoreSubmoduleMode... expectedEmptyModes) throws IOException {
+		boolean diffResult = indexDiff.diff();
+		Set<String> submodulePaths = indexDiff
+				.getPathsWithIndexMode(FileMode.GITLINK);
+		boolean emptyExpected = false;
+		for (IgnoreSubmoduleMode empty : expectedEmptyModes) {
+			if (mode.equals(empty)) {
+				emptyExpected = true;
+				break;
+			}
+		}
+		if (emptyExpected) {
+			assertFalse("diff should be false with mode=" + mode,
+					diffResult);
+			assertEquals("should have no paths with FileMode.GITLINK", 0,
+					submodulePaths.size());
+		} else {
+			assertTrue("diff should be true with mode=" + mode,
+					diffResult);
+			assertTrue("submodule path should have FileMode.GITLINK",
+					submodulePaths.contains("modules/submodule"));
+		}
+	}
+
 	@Theory
 	public void testDirtySubmoduleWorktree(IgnoreSubmoduleMode mode)
 			throws IOException {
@@ -125,13 +152,8 @@
 		IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD,
 				new FileTreeIterator(db));
 		indexDiff.setIgnoreSubmoduleMode(mode);
-		if (mode.equals(IgnoreSubmoduleMode.ALL)
-				|| mode.equals(IgnoreSubmoduleMode.DIRTY))
-			assertFalse("diff should be false with mode=" + mode,
-					indexDiff.diff());
-		else
-			assertTrue("diff should be true with mode=" + mode,
-					indexDiff.diff());
+		assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL,
+				IgnoreSubmoduleMode.DIRTY);
 	}
 
 	@Theory
@@ -145,12 +167,7 @@
 		IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD,
 				new FileTreeIterator(db));
 		indexDiff.setIgnoreSubmoduleMode(mode);
-		if (mode.equals(IgnoreSubmoduleMode.ALL))
-			assertFalse("diff should be false with mode=" + mode,
-					indexDiff.diff());
-		else
-			assertTrue("diff should be true with mode=" + mode,
-					indexDiff.diff());
+		assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL);
 	}
 
 	@Theory
@@ -163,13 +180,8 @@
 		IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD,
 				new FileTreeIterator(db));
 		indexDiff.setIgnoreSubmoduleMode(mode);
-		if (mode.equals(IgnoreSubmoduleMode.ALL)
-				|| mode.equals(IgnoreSubmoduleMode.DIRTY))
-			assertFalse("diff should be false with mode=" + mode,
-					indexDiff.diff());
-		else
-			assertTrue("diff should be true with mode=" + mode,
-					indexDiff.diff());
+		assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL,
+				IgnoreSubmoduleMode.DIRTY);
 	}
 
 	@Theory
@@ -183,13 +195,8 @@
 		IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD,
 				new FileTreeIterator(db));
 		indexDiff.setIgnoreSubmoduleMode(mode);
-		if (mode.equals(IgnoreSubmoduleMode.ALL)
-				|| mode.equals(IgnoreSubmoduleMode.DIRTY))
-			assertFalse("diff should be false with mode=" + mode,
-					indexDiff.diff());
-		else
-			assertTrue("diff should be true with mode=" + mode,
-					indexDiff.diff());
+		assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL,
+				IgnoreSubmoduleMode.DIRTY);
 	}
 
 	@Theory
@@ -200,13 +207,7 @@
 		IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD,
 				new FileTreeIterator(db));
 		indexDiff.setIgnoreSubmoduleMode(mode);
-		if (mode.equals(IgnoreSubmoduleMode.ALL)
-				|| mode.equals(IgnoreSubmoduleMode.DIRTY)
-				|| mode.equals(IgnoreSubmoduleMode.UNTRACKED))
-			assertFalse("diff should be false with mode=" + mode,
-					indexDiff.diff());
-		else
-			assertTrue("diff should be true with mode=" + mode,
-					indexDiff.diff());
+		assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL,
+				IgnoreSubmoduleMode.DIRTY, IgnoreSubmoduleMode.UNTRACKED);
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java
index 0cada5c..a0cf0d2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java
@@ -51,6 +51,7 @@
 import static org.junit.Assert.assertTrue;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -498,24 +499,48 @@
 	}
 
 	@Test
-	public void singlePushInsteadOf() throws Exception {
+	public void pushInsteadOfNotAppliedToPushUri() throws Exception {
 		config.setString("remote", "origin", "pushurl", "short:project.git");
 		config.setString("url", "https://server/repos/", "pushInsteadOf",
 				"short:");
 		RemoteConfig rc = new RemoteConfig(config, "origin");
 		assertFalse(rc.getPushURIs().isEmpty());
+		assertEquals("short:project.git",
+				rc.getPushURIs().get(0).toASCIIString());
+	}
+
+	@Test
+	public void pushInsteadOfAppliedToUri() throws Exception {
+		config.setString("remote", "origin", "url", "short:project.git");
+		config.setString("url", "https://server/repos/", "pushInsteadOf",
+				"short:");
+		RemoteConfig rc = new RemoteConfig(config, "origin");
+		assertFalse(rc.getPushURIs().isEmpty());
+		assertEquals("https://server/repos/project.git",
+				rc.getPushURIs().get(0).toASCIIString());
+	}
+
+	@Test
+	public void multiplePushInsteadOf() throws Exception {
+		config.setString("remote", "origin", "url", "prefixproject.git");
+		config.setStringList("url", "https://server/repos/", "pushInsteadOf",
+				Arrays.asList("pre", "prefix", "pref", "perf"));
+		RemoteConfig rc = new RemoteConfig(config, "origin");
+		assertFalse(rc.getPushURIs().isEmpty());
 		assertEquals("https://server/repos/project.git", rc.getPushURIs()
 				.get(0).toASCIIString());
 	}
 
 	@Test
-	public void multiplePushInsteadOf() throws Exception {
-		config.setString("remote", "origin", "pushurl", "prefixproject.git");
-		config.setStringList("url", "https://server/repos/", "pushInsteadOf",
-				Arrays.asList("pre", "prefix", "pref", "perf"));
+	public void pushInsteadOfNoPushUrl() throws Exception {
+		config.setString("remote", "origin", "url",
+				"http://git.eclipse.org/gitroot/jgit/jgit");
+		config.setStringList("url", "ssh://someone@git.eclipse.org:29418/",
+				"pushInsteadOf",
+				Collections.singletonList("http://git.eclipse.org/gitroot/"));
 		RemoteConfig rc = new RemoteConfig(config, "origin");
 		assertFalse(rc.getPushURIs().isEmpty());
-		assertEquals("https://server/repos/project.git", rc.getPushURIs()
-				.get(0).toASCIIString());
+		assertEquals("ssh://someone@git.eclipse.org:29418/jgit/jgit",
+				rc.getPushURIs().get(0).toASCIIString());
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/LongMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LongMapTest.java
similarity index 98%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/LongMapTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LongMapTest.java
index 1a86aaf..054c61e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/LongMapTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LongMapTest.java
@@ -41,7 +41,7 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.transport;
+package org.eclipse.jgit.util;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java
index 7e11a61..d2d44ff 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java
@@ -90,6 +90,24 @@
 	}
 
 	@Test
+	public void testDecodeUInt24() {
+		assertEquals(0, NB.decodeUInt24(b(0, 0, 0), 0));
+		assertEquals(0, NB.decodeUInt24(padb(3, 0, 0, 0), 3));
+
+		assertEquals(3, NB.decodeUInt24(b(0, 0, 3), 0));
+		assertEquals(3, NB.decodeUInt24(padb(3, 0, 0, 3), 3));
+
+		assertEquals(0xcede03, NB.decodeUInt24(b(0xce, 0xde, 3), 0));
+		assertEquals(0xbade03, NB.decodeUInt24(padb(3, 0xba, 0xde, 3), 3));
+
+		assertEquals(0x03bade, NB.decodeUInt24(b(3, 0xba, 0xde), 0));
+		assertEquals(0x03bade, NB.decodeUInt24(padb(3, 3, 0xba, 0xde), 3));
+
+		assertEquals(0xffffff, NB.decodeUInt24(b(0xff, 0xff, 0xff), 0));
+		assertEquals(0xffffff, NB.decodeUInt24(padb(3, 0xff, 0xff, 0xff), 3));
+	}
+
+	@Test
 	public void testDecodeInt32() {
 		assertEquals(0, NB.decodeInt32(b(0, 0, 0, 0), 0));
 		assertEquals(0, NB.decodeInt32(padb(3, 0, 0, 0, 0), 3));
@@ -198,6 +216,39 @@
 	}
 
 	@Test
+	public void testEncodeInt24() {
+		byte[] out = new byte[16];
+
+		prepareOutput(out);
+		NB.encodeInt24(out, 0, 0);
+		assertOutput(b(0, 0, 0), out, 0);
+
+		prepareOutput(out);
+		NB.encodeInt24(out, 3, 0);
+		assertOutput(b(0, 0, 0), out, 3);
+
+		prepareOutput(out);
+		NB.encodeInt24(out, 0, 3);
+		assertOutput(b(0, 0, 3), out, 0);
+
+		prepareOutput(out);
+		NB.encodeInt24(out, 3, 3);
+		assertOutput(b(0, 0, 3), out, 3);
+
+		prepareOutput(out);
+		NB.encodeInt24(out, 0, 0xc0deac);
+		assertOutput(b(0xc0, 0xde, 0xac), out, 0);
+
+		prepareOutput(out);
+		NB.encodeInt24(out, 3, 0xbadeac);
+		assertOutput(b(0xba, 0xde, 0xac), out, 3);
+
+		prepareOutput(out);
+		NB.encodeInt24(out, 3, -1);
+		assertOutput(b(0xff, 0xff, 0xff), out, 3);
+	}
+
+	@Test
 	public void testEncodeInt32() {
 		final byte[] out = new byte[16];
 
@@ -315,10 +366,24 @@
 		return r;
 	}
 
+	private static byte[] b(int a, int b, int c) {
+		return new byte[] { (byte) a, (byte) b, (byte) c };
+	}
+
 	private static byte[] b(final int a, final int b, final int c, final int d) {
 		return new byte[] { (byte) a, (byte) b, (byte) c, (byte) d };
 	}
 
+	private static byte[] padb(int len, int a, int b, int c) {
+		final byte[] r = new byte[len + 4];
+		for (int i = 0; i < len; i++)
+			r[i] = (byte) 0xaf;
+		r[len] = (byte) a;
+		r[len + 1] = (byte) b;
+		r[len + 2] = (byte) c;
+		return r;
+	}
+
 	private static byte[] padb(final int len, final int a, final int b,
 			final int c, final int d) {
 		final byte[] r = new byte[len + 4];
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
index 21d6283..6b20da3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
@@ -47,8 +47,10 @@
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 import org.eclipse.jgit.api.CheckoutResult.Status;
 import org.eclipse.jgit.api.errors.CheckoutConflictException;
@@ -66,6 +68,7 @@
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.errors.UnmergedPathException;
+import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -175,6 +178,8 @@
 
 	private boolean checkoutAllPaths;
 
+	private Set<String> actuallyModifiedPaths;
+
 	/**
 	 * @param repo
 	 */
@@ -410,7 +415,8 @@
 	}
 
 	/**
-	 * Checkout paths into index and working directory
+	 * Checkout paths into index and working directory, firing a
+	 * {@link WorkingTreeModifiedEvent} if the working tree was modified.
 	 *
 	 * @return this instance
 	 * @throws IOException
@@ -418,6 +424,7 @@
 	 */
 	protected CheckoutCommand checkoutPaths() throws IOException,
 			RefNotFoundException {
+		actuallyModifiedPaths = new HashSet<>();
 		DirCache dc = repo.lockDirCache();
 		try (RevWalk revWalk = new RevWalk(repo);
 				TreeWalk treeWalk = new TreeWalk(repo,
@@ -432,7 +439,16 @@
 				checkoutPathsFromCommit(treeWalk, dc, commit);
 			}
 		} finally {
-			dc.unlock();
+			try {
+				dc.unlock();
+			} finally {
+				WorkingTreeModifiedEvent event = new WorkingTreeModifiedEvent(
+						actuallyModifiedPaths, null);
+				actuallyModifiedPaths = null;
+				if (!event.isEmpty()) {
+					repo.fireEvent(event);
+				}
+			}
 		}
 		return this;
 	}
@@ -461,9 +477,11 @@
 					int stage = ent.getStage();
 					if (stage > DirCacheEntry.STAGE_0) {
 						if (checkoutStage != null) {
-							if (stage == checkoutStage.number)
+							if (stage == checkoutStage.number) {
 								checkoutPath(ent, r, new CheckoutMetadata(
 										eolStreamType, filterCommand));
+								actuallyModifiedPaths.add(path);
+							}
 						} else {
 							UnmergedPathException e = new UnmergedPathException(
 									ent);
@@ -472,6 +490,7 @@
 					} else {
 						checkoutPath(ent, r, new CheckoutMetadata(eolStreamType,
 								filterCommand));
+						actuallyModifiedPaths.add(path);
 					}
 				}
 			});
@@ -492,13 +511,15 @@
 			final EolStreamType eolStreamType = treeWalk.getEolStreamType();
 			final String filterCommand = treeWalk
 					.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE);
-			editor.add(new PathEdit(treeWalk.getPathString()) {
+			final String path = treeWalk.getPathString();
+			editor.add(new PathEdit(path) {
 				@Override
 				public void apply(DirCacheEntry ent) {
 					ent.setObjectId(blobId);
 					ent.setFileMode(mode);
 					checkoutPath(ent, r,
 							new CheckoutMetadata(eolStreamType, filterCommand));
+					actuallyModifiedPaths.add(path);
 				}
 			});
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java
index c58efb1..e41a03b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java
@@ -54,6 +54,7 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
@@ -135,6 +136,10 @@
 				}
 		} catch (IOException e) {
 			throw new JGitInternalException(e.getMessage(), e);
+		} finally {
+			if (!files.isEmpty()) {
+				repo.fireEvent(new WorkingTreeModifiedEvent(null, files));
+			}
 		}
 		return files;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
index bae54ce..75460fb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -64,6 +64,7 @@
 import org.eclipse.jgit.api.errors.NoMessageException;
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
 import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config.ConfigEnum;
@@ -355,6 +356,10 @@
 							.getMergeResults();
 					failingPaths = resolveMerger.getFailingPaths();
 					unmergedPaths = resolveMerger.getUnmergedPaths();
+					if (!resolveMerger.getModifiedFiles().isEmpty()) {
+						repo.fireEvent(new WorkingTreeModifiedEvent(
+								resolveMerger.getModifiedFiles(), null));
+					}
 				} else
 					noProblems = merger.merge(headCommit, srcCommit);
 				refLogMessage.append(": Merge made by "); //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java
index 9e2cf31..48c23f5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java
@@ -44,8 +44,10 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedList;
+import java.util.List;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
@@ -53,6 +55,7 @@
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuildIterator;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
@@ -145,6 +148,7 @@
 		checkCallable();
 		DirCache dc = null;
 
+		List<String> actuallyDeletedFiles = new ArrayList<>();
 		try (final TreeWalk tw = new TreeWalk(repo)) {
 			dc = repo.lockDirCache();
 			DirCacheBuilder builder = dc.builder();
@@ -157,11 +161,14 @@
 				if (!cached) {
 					final FileMode mode = tw.getFileMode(0);
 					if (mode.getObjectType() == Constants.OBJ_BLOB) {
+						String relativePath = tw.getPathString();
 						final File path = new File(repo.getWorkTree(),
-								tw.getPathString());
+								relativePath);
 						// Deleting a blob is simply a matter of removing
 						// the file or symlink named by the tree entry.
-						delete(path);
+						if (delete(path)) {
+							actuallyDeletedFiles.add(relativePath);
+						}
 					}
 				}
 			}
@@ -171,16 +178,28 @@
 			throw new JGitInternalException(
 					JGitText.get().exceptionCaughtDuringExecutionOfRmCommand, e);
 		} finally {
-			if (dc != null)
-				dc.unlock();
+			try {
+				if (dc != null) {
+					dc.unlock();
+				}
+			} finally {
+				if (!actuallyDeletedFiles.isEmpty()) {
+					repo.fireEvent(new WorkingTreeModifiedEvent(null,
+							actuallyDeletedFiles));
+				}
+			}
 		}
 
 		return dc;
 	}
 
-	private void delete(File p) {
-		while (p != null && !p.equals(repo.getWorkTree()) && p.delete())
+	private boolean delete(File p) {
+		boolean deleted = false;
+		while (p != null && !p.equals(repo.getWorkTree()) && p.delete()) {
+			deleted = true;
 			p = p.getParentFile();
+		}
+		return deleted;
 	}
 
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 10ec2a6..b56fb25 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012, GitHub Inc.
+ * Copyright (C) 2012, 2017 GitHub Inc.
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -44,6 +44,9 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidRefNameException;
@@ -58,6 +61,7 @@
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CheckoutConflictException;
+import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
@@ -198,7 +202,13 @@
 					"stash" }); //$NON-NLS-1$
 			merger.setBase(stashHeadCommit);
 			merger.setWorkingTreeIterator(new FileTreeIterator(repo));
-			if (merger.merge(headCommit, stashCommit)) {
+			boolean mergeSucceeded = merger.merge(headCommit, stashCommit);
+			List<String> modifiedByMerge = merger.getModifiedFiles();
+			if (!modifiedByMerge.isEmpty()) {
+				repo.fireEvent(
+						new WorkingTreeModifiedEvent(modifiedByMerge, null));
+			}
+			if (mergeSucceeded) {
 				DirCache dc = repo.lockDirCache();
 				DirCacheCheckout dco = new DirCacheCheckout(repo, headTree,
 						dc, merger.getResultTreeId());
@@ -329,6 +339,7 @@
 
 	private void resetUntracked(RevTree tree) throws CheckoutConflictException,
 			IOException {
+		Set<String> actuallyModifiedPaths = new HashSet<>();
 		// TODO maybe NameConflictTreeWalk ?
 		try (TreeWalk walk = new TreeWalk(repo)) {
 			walk.addTree(tree);
@@ -361,6 +372,12 @@
 
 				checkoutPath(entry, reader,
 						new CheckoutMetadata(eolStreamType, null));
+				actuallyModifiedPaths.add(entry.getPathString());
+			}
+		} finally {
+			if (!actuallyModifiedPaths.isEmpty()) {
+				repo.fireEvent(new WorkingTreeModifiedEvent(
+						actuallyModifiedPaths, null));
 			}
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
index 681f8e6..21b06e6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java
@@ -62,6 +62,7 @@
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.UnmergedPathException;
+import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
@@ -240,6 +241,7 @@
 	public RevCommit call() throws GitAPIException {
 		checkCallable();
 
+		List<String> deletedFiles = new ArrayList<>();
 		Ref head = getHead();
 		try (ObjectReader reader = repo.newObjectReader()) {
 			RevCommit headCommit = parseCommit(reader, head.getObjectId());
@@ -377,9 +379,11 @@
 				// Remove untracked files
 				if (includeUntracked) {
 					for (DirCacheEntry entry : untracked) {
+						String repoRelativePath = entry.getPathString();
 						File file = new File(repo.getWorkTree(),
-								entry.getPathString());
+								repoRelativePath);
 						FileUtils.delete(file);
+						deletedFiles.add(repoRelativePath);
 					}
 				}
 
@@ -394,6 +398,11 @@
 			return parseCommit(reader, commitId);
 		} catch (IOException e) {
 			throw new JGitInternalException(JGitText.get().stashFailed, e);
+		} finally {
+			if (!deletedFiles.isEmpty()) {
+				repo.fireEvent(
+						new WorkingTreeModifiedEvent(null, deletedFiles));
+			}
 		}
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index aed76ac..f8c23ca 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -50,6 +50,7 @@
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
@@ -61,6 +62,7 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.IndexWriteException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
@@ -85,6 +87,7 @@
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.IntList;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.SystemReader;
 import org.eclipse.jgit.util.io.EolStreamTypeUtil;
@@ -151,6 +154,8 @@
 
 	private boolean emptyDirCache;
 
+	private boolean performingCheckout;
+
 	/**
 	 * @return a list of updated paths and smudgeFilterCommands
 	 */
@@ -432,7 +437,8 @@
 	}
 
 	/**
-	 * Execute this checkout
+	 * Execute this checkout. A {@link WorkingTreeModifiedEvent} is fired if the
+	 * working tree was modified; even if the checkout fails.
 	 *
 	 * @return <code>false</code> if this method could not delete all the files
 	 *         which should be deleted (e.g. because of of the files was
@@ -448,7 +454,17 @@
 		try {
 			return doCheckout();
 		} finally {
-			dc.unlock();
+			try {
+				dc.unlock();
+			} finally {
+				if (performingCheckout) {
+					WorkingTreeModifiedEvent event = new WorkingTreeModifiedEvent(
+							getUpdated().keySet(), getRemoved());
+					if (!event.isEmpty()) {
+						repo.fireEvent(event);
+					}
+				}
+			}
 		}
 	}
 
@@ -472,11 +488,13 @@
 			// update our index
 			builder.finish();
 
+			performingCheckout = true;
 			File file = null;
 			String last = null;
 			// when deleting files process them in the opposite order as they have
 			// been reported. This ensures the files are deleted before we delete
 			// their parent folders
+			IntList nonDeleted = new IntList();
 			for (int i = removed.size() - 1; i >= 0; i--) {
 				String r = removed.get(i);
 				file = new File(repo.getWorkTree(), r);
@@ -486,25 +504,47 @@
 					// a submodule, in which case we shall not attempt
 					// to delete it. A submodule is not empty, so it
 					// is safe to check this after a failed delete.
-					if (!repo.getFS().isDirectory(file))
+					if (!repo.getFS().isDirectory(file)) {
+						nonDeleted.add(i);
 						toBeDeleted.add(r);
+					}
 				} else {
 					if (last != null && !isSamePrefix(r, last))
 						removeEmptyParents(new File(repo.getWorkTree(), last));
 					last = r;
 				}
 			}
-			if (file != null)
+			if (file != null) {
 				removeEmptyParents(file);
-
-			for (Map.Entry<String, CheckoutMetadata> e : updated.entrySet()) {
-				String path = e.getKey();
-				CheckoutMetadata meta = e.getValue();
-				DirCacheEntry entry = dc.getEntry(path);
-				if (!FileMode.GITLINK.equals(entry.getRawMode()))
-					checkoutEntry(repo, entry, objectReader, false, meta);
 			}
-
+			removed = filterOut(removed, nonDeleted);
+			nonDeleted = null;
+			Iterator<Map.Entry<String, CheckoutMetadata>> toUpdate = updated
+					.entrySet().iterator();
+			Map.Entry<String, CheckoutMetadata> e = null;
+			try {
+				while (toUpdate.hasNext()) {
+					e = toUpdate.next();
+					String path = e.getKey();
+					CheckoutMetadata meta = e.getValue();
+					DirCacheEntry entry = dc.getEntry(path);
+					if (!FileMode.GITLINK.equals(entry.getRawMode())) {
+						checkoutEntry(repo, entry, objectReader, false, meta);
+					}
+					e = null;
+				}
+			} catch (Exception ex) {
+				// We didn't actually modify the current entry nor any that
+				// might follow.
+				if (e != null) {
+					toUpdate.remove();
+				}
+				while (toUpdate.hasNext()) {
+					e = toUpdate.next();
+					toUpdate.remove();
+				}
+				throw ex;
+			}
 			// commit the index builder - a new index is persisted
 			if (!builder.commit())
 				throw new IndexWriteException();
@@ -512,6 +552,36 @@
 		return toBeDeleted.size() == 0;
 	}
 
+	private static ArrayList<String> filterOut(ArrayList<String> strings,
+			IntList indicesToRemove) {
+		int n = indicesToRemove.size();
+		if (n == strings.size()) {
+			return new ArrayList<>(0);
+		}
+		switch (n) {
+		case 0:
+			return strings;
+		case 1:
+			strings.remove(indicesToRemove.get(0));
+			return strings;
+		default:
+			int length = strings.size();
+			ArrayList<String> result = new ArrayList<>(length - n);
+			// Process indicesToRemove from the back; we know that it
+			// contains indices in descending order.
+			int j = n - 1;
+			int idx = indicesToRemove.get(j);
+			for (int i = 0; i < length; i++) {
+				if (i == idx) {
+					idx = (--j >= 0) ? indicesToRemove.get(j) : -1;
+				} else {
+					result.add(strings.get(i));
+				}
+			}
+			return result;
+		}
+	}
+
 	private static boolean isSamePrefix(String a, String b) {
 		int as = a.lastIndexOf('/');
 		int bs = b.lastIndexOf('/');
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java
index 12ef533..cea03db 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java
@@ -53,6 +53,19 @@
 	private final ConcurrentMap<Class<? extends RepositoryListener>, CopyOnWriteArrayList<ListenerHandle>> lists = new ConcurrentHashMap<>();
 
 	/**
+	 * Register a {@link WorkingTreeModifiedListener}.
+	 *
+	 * @param listener
+	 *            the listener implementation.
+	 * @return handle to later remove the listener.
+	 * @since 4.9
+	 */
+	public ListenerHandle addWorkingTreeModifiedListener(
+			WorkingTreeModifiedListener listener) {
+		return addListener(WorkingTreeModifiedListener.class, listener);
+	}
+
+	/**
 	 * Register an IndexChangedListener.
 	 *
 	 * @param listener
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java
new file mode 100644
index 0000000..7a53233
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.events;
+
+import java.util.Collections;
+import java.util.Collection;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A {@link RepositoryEvent} describing changes to the working tree. It is fired
+ * whenever a {@link org.eclipse.jgit.dircache.DirCacheCheckout} modifies
+ * (adds/deletes/updates) files in the working tree.
+ *
+ * @since 4.9
+ */
+public class WorkingTreeModifiedEvent
+		extends RepositoryEvent<WorkingTreeModifiedListener> {
+
+	private Collection<String> modified;
+
+	private Collection<String> deleted;
+
+	/**
+	 * Creates a new {@link WorkingTreeModifiedEvent} with the given
+	 * collections.
+	 *
+	 * @param modified
+	 *            repository-relative paths that were added or updated
+	 * @param deleted
+	 *            repository-relative paths that were deleted
+	 */
+	public WorkingTreeModifiedEvent(Collection<String> modified,
+			Collection<String> deleted) {
+		this.modified = modified;
+		this.deleted = deleted;
+	}
+
+	/**
+	 * Determines whether there are any changes recorded in this event.
+	 *
+	 * @return {@code true} if no files were modified or deleted, {@code false}
+	 *         otherwise
+	 */
+	public boolean isEmpty() {
+		return (modified == null || modified.isEmpty())
+				&& (deleted == null || deleted.isEmpty());
+	}
+
+	/**
+	 * Retrieves the {@link Collection} of repository-relative paths of files
+	 * that were modified (added or updated).
+	 *
+	 * @return the set
+	 */
+	public @NonNull Collection<String> getModified() {
+		Collection<String> result = modified;
+		if (result == null) {
+			result = Collections.emptyList();
+			modified = result;
+		}
+		return result;
+	}
+
+	/**
+	 * Retrieves the {@link Collection} of repository-relative paths of files
+	 * that were deleted.
+	 *
+	 * @return the set
+	 */
+	public @NonNull Collection<String> getDeleted() {
+		Collection<String> result = deleted;
+		if (result == null) {
+			result = Collections.emptyList();
+			deleted = result;
+		}
+		return result;
+	}
+
+	@Override
+	public Class<WorkingTreeModifiedListener> getListenerType() {
+		return WorkingTreeModifiedListener.class;
+	}
+
+	@Override
+	public void dispatch(WorkingTreeModifiedListener listener) {
+		listener.onWorkingTreeModified(this);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedListener.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedListener.java
new file mode 100644
index 0000000..402a900
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedListener.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.events;
+
+/**
+ * Receives {@link WorkingTreeModifiedEvent}s, which are fired whenever a
+ * {@link org.eclipse.jgit.dircache.DirCacheCheckout} modifies
+ * (adds/deletes/updates) files in the working tree.
+ *
+ * @since 4.9
+ */
+public interface WorkingTreeModifiedListener extends RepositoryListener {
+
+	/**
+	 * Respond to working tree modifications.
+	 *
+	 * @param event
+	 */
+	void onWorkingTreeModified(WorkingTreeModifiedEvent event);
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
index 6a674aa..646feac 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
@@ -216,7 +216,7 @@
 				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
 
 		String reftype = repoConfig.getString(
-				"extensions", null, "refsStorage"); //$NON-NLS-1$ //$NON-NLS-2$
+				"extensions", null, "refStorage"); //$NON-NLS-1$ //$NON-NLS-2$
 		if (repositoryFormatVersion >= 1 && reftype != null) {
 			if (StringUtils.equalsIgnoreCase(reftype, "reftree")) { //$NON-NLS-1$
 				refs = new RefTreeDatabase(this, new RefDirectory(this));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
index fcc47fb..0611d3e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
@@ -1108,8 +1108,17 @@
 		if (invalid || invalidBitmap)
 			return null;
 		if (bitmapIdx == null && hasExt(BITMAP_INDEX)) {
-			final PackBitmapIndex idx = PackBitmapIndex.open(
-					extFile(BITMAP_INDEX), idx(), getReverseIdx());
+			final PackBitmapIndex idx;
+			try {
+				idx = PackBitmapIndex.open(extFile(BITMAP_INDEX), idx(),
+						getReverseIdx());
+			} catch (FileNotFoundException e) {
+				// Once upon a time this bitmap file existed. Now it
+				// has been removed. Most likely an external gc  has
+				// removed this packfile and the bitmap
+				 invalidBitmap = true;
+				 return null;
+			}
 
 			// At this point, idx() will have set packChecksum.
 			if (Arrays.equals(packChecksum, idx.packChecksum))
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
index e544b72..ea573a4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
@@ -513,14 +513,10 @@
 					}
 				}
 
-				for (int i = 0; i < treeWalk.getTreeCount(); i++) {
-					Set<String> values = fileModes.get(treeWalk.getFileMode(i));
-					String path = treeWalk.getPathString();
-					if (path != null) {
-						if (values == null)
-							values = new HashSet<>();
-						values.add(path);
-						fileModes.put(treeWalk.getFileMode(i), values);
+				String path = treeWalk.getPathString();
+				if (path != null) {
+					for (int i = 0; i < treeWalk.getTreeCount(); i++) {
+						recordFileMode(path, treeWalk.getFileMode(i));
 					}
 				}
 			}
@@ -545,19 +541,21 @@
 				}
 				Repository subRepo = smw.getRepository();
 				if (subRepo != null) {
+					String subRepoPath = smw.getPath();
 					try {
 						ObjectId subHead = subRepo.resolve("HEAD"); //$NON-NLS-1$
 						if (subHead != null
-								&& !subHead.equals(smw.getObjectId()))
-							modified.add(smw.getPath());
-						else if (ignoreSubmoduleMode != IgnoreSubmoduleMode.DIRTY) {
+								&& !subHead.equals(smw.getObjectId())) {
+							modified.add(subRepoPath);
+							recordFileMode(subRepoPath, FileMode.GITLINK);
+						} else if (ignoreSubmoduleMode != IgnoreSubmoduleMode.DIRTY) {
 							IndexDiff smid = submoduleIndexDiffs.get(smw
 									.getPath());
 							if (smid == null) {
 								smid = new IndexDiff(subRepo,
 										smw.getObjectId(),
 										wTreeIt.getWorkingTreeIterator(subRepo));
-								submoduleIndexDiffs.put(smw.getPath(), smid);
+								submoduleIndexDiffs.put(subRepoPath, smid);
 							}
 							if (smid.diff()) {
 								if (ignoreSubmoduleMode == IgnoreSubmoduleMode.UNTRACKED
@@ -569,7 +567,8 @@
 										&& smid.getRemoved().isEmpty()) {
 									continue;
 								}
-								modified.add(smw.getPath());
+								modified.add(subRepoPath);
+								recordFileMode(subRepoPath, FileMode.GITLINK);
 							}
 						}
 					} finally {
@@ -593,6 +592,17 @@
 			return true;
 	}
 
+	private void recordFileMode(String path, FileMode mode) {
+		Set<String> values = fileModes.get(mode);
+		if (path != null) {
+			if (values == null) {
+				values = new HashSet<>();
+				fileModes.put(mode, values);
+			}
+			values.add(path);
+		}
+	}
+
 	private boolean isEntryGitLink(AbstractTreeIterator ti) {
 		return ((ti != null) && (ti.getEntryRawMode() == FileMode.GITLINK
 				.getBits()));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
index db3578b..2f6b271 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
@@ -82,6 +82,7 @@
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.util.BlockList;
 import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.LongMap;
 import org.eclipse.jgit.util.NB;
 import org.eclipse.jgit.util.sha1.SHA1;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
index d91684e..a0d81c0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
@@ -170,16 +170,28 @@
 		vlst = rc.getStringList(SECTION, name, KEY_URL);
 		Map<String, String> insteadOf = getReplacements(rc, KEY_INSTEADOF);
 		uris = new ArrayList<>(vlst.length);
-		for (final String s : vlst)
+		for (final String s : vlst) {
 			uris.add(new URIish(replaceUri(s, insteadOf)));
-
-		Map<String, String> pushInsteadOf = getReplacements(rc,
-				KEY_PUSHINSTEADOF);
-		vlst = rc.getStringList(SECTION, name, KEY_PUSHURL);
-		pushURIs = new ArrayList<>(vlst.length);
-		for (final String s : vlst)
-			pushURIs.add(new URIish(replaceUri(s, pushInsteadOf)));
-
+		}
+		String[] plst = rc.getStringList(SECTION, name, KEY_PUSHURL);
+		pushURIs = new ArrayList<>(plst.length);
+		for (final String s : plst) {
+			pushURIs.add(new URIish(s));
+		}
+		if (pushURIs.isEmpty()) {
+			// Would default to the uris. If we have pushinsteadof, we must
+			// supply rewritten push uris.
+			Map<String, String> pushInsteadOf = getReplacements(rc,
+					KEY_PUSHINSTEADOF);
+			if (!pushInsteadOf.isEmpty()) {
+				for (String s : vlst) {
+					String replaced = replaceUri(s, pushInsteadOf);
+					if (!s.equals(replaced)) {
+						pushURIs.add(new URIish(replaced));
+					}
+				}
+			}
+		}
 		vlst = rc.getStringList(SECTION, name, KEY_FETCH);
 		fetch = new ArrayList<>(vlst.length);
 		for (final String s : vlst)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/LongMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LongMap.java
similarity index 82%
rename from org.eclipse.jgit/src/org/eclipse/jgit/transport/LongMap.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/util/LongMap.java
index 4d60202..7b0b0c7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/LongMap.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LongMap.java
@@ -41,15 +41,16 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.transport;
+package org.eclipse.jgit.util;
 
 /**
- * Simple Map<long,Object> helper for {@link PackParser}.
+ * Simple Map<long,Object>.
  *
  * @param <V>
  *            type of the value instance.
+ * @since 4.9
  */
-final class LongMap<V> {
+public class LongMap<V> {
 	private static final float LOAD_FACTOR = 0.75f;
 
 	private Node<V>[] table;
@@ -60,16 +61,27 @@
 	/** Next {@link #size} to trigger a {@link #grow()}. */
 	private int growAt;
 
-	LongMap() {
+	/** Initialize an empty LongMap. */
+	public LongMap() {
 		table = createArray(64);
 		growAt = (int) (table.length * LOAD_FACTOR);
 	}
 
-	boolean containsKey(final long key) {
+	/**
+	 * @param key
+	 *            the key to find.
+	 * @return {@code true} if {@code key} is present in the map.
+	 */
+	public boolean containsKey(long key) {
 		return get(key) != null;
 	}
 
-	V get(final long key) {
+	/**
+	 * @param key
+	 *            the key to find.
+	 * @return stored value of the key, or {@code null}.
+	 */
+	public V get(long key) {
 		for (Node<V> n = table[index(key)]; n != null; n = n.next) {
 			if (n.key == key)
 				return n.value;
@@ -77,7 +89,12 @@
 		return null;
 	}
 
-	V remove(final long key) {
+	/**
+	 * @param key
+	 *            key to remove from the map.
+	 * @return old value of the key, or {@code null}.
+	 */
+	public V remove(long key) {
 		Node<V> n = table[index(key)];
 		Node<V> prior = null;
 		while (n != null) {
@@ -95,7 +112,14 @@
 		return null;
 	}
 
-	V put(final long key, final V value) {
+	/**
+	 * @param key
+	 *            key to store {@code value} under.
+	 * @param value
+	 *            new value.
+	 * @return prior value, or null.
+	 */
+	public V put(long key, V value) {
 		for (Node<V> n = table[index(key)]; n != null; n = n.next) {
 			if (n.key == key) {
 				final V o = n.value;
@@ -145,9 +169,7 @@
 
 	private static class Node<V> {
 		final long key;
-
 		V value;
-
 		Node<V> next;
 
 		Node(final long k, final V v) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java
index 8536f1d..471a499 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java
@@ -113,6 +113,24 @@
 	}
 
 	/**
+	 * Convert sequence of 3 bytes (network byte order) into unsigned value.
+	 *
+	 * @param intbuf
+	 *            buffer to acquire the 3 bytes of data from.
+	 * @param offset
+	 *            position within the buffer to begin reading from. This
+	 *            position and the next 2 bytes after it (for a total of 3
+	 *            bytes) will be read.
+	 * @return signed integer value that matches the 24 bits read.
+	 * @since 4.9
+	 */
+	public static int decodeUInt24(byte[] intbuf, int offset) {
+		int r = (intbuf[offset] & 0xff) << 8;
+		r |= intbuf[offset + 1] & 0xff;
+		return (r << 8) | (intbuf[offset + 2] & 0xff);
+	}
+
+	/**
 	 * Convert sequence of 4 bytes (network byte order) into signed value.
 	 *
 	 * @param intbuf
@@ -223,6 +241,29 @@
 	}
 
 	/**
+	 * Write a 24 bit integer as a sequence of 3 bytes (network byte order).
+	 *
+	 * @param intbuf
+	 *            buffer to write the 3 bytes of data into.
+	 * @param offset
+	 *            position within the buffer to begin writing to. This position
+	 *            and the next 2 bytes after it (for a total of 3 bytes) will be
+	 *            replaced.
+	 * @param v
+	 *            the value to write.
+	 * @since 4.9
+	 */
+	public static void encodeInt24(byte[] intbuf, int offset, int v) {
+		intbuf[offset + 2] = (byte) v;
+		v >>>= 8;
+
+		intbuf[offset + 1] = (byte) v;
+		v >>>= 8;
+
+		intbuf[offset] = (byte) v;
+	}
+
+	/**
 	 * Write a 32 bit integer as a sequence of 4 bytes (network byte order).
 	 *
 	 * @param intbuf
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java
index c95992f..727c1f4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java
@@ -144,6 +144,11 @@
 
 	private static EolStreamType checkInStreamType(WorkingTreeOptions options,
 			Attributes attrs) {
+		if (attrs.isUnset("text")) {//$NON-NLS-1$
+			// "binary" or "-text" (which is included in the binary expansion)
+			return EolStreamType.DIRECT;
+		}
+
 		// old git system
 		if (attrs.isSet("crlf")) {//$NON-NLS-1$
 			return EolStreamType.TEXT_LF;
@@ -154,9 +159,6 @@
 		}
 
 		// new git system
-		if (attrs.isUnset("text")) {//$NON-NLS-1$
-			return EolStreamType.DIRECT;
-		}
 		String eol = attrs.getValue("eol"); //$NON-NLS-1$
 		if (eol != null)
 			// check-in is always normalized to LF
@@ -183,6 +185,11 @@
 
 	private static EolStreamType checkOutStreamType(WorkingTreeOptions options,
 			Attributes attrs) {
+		if (attrs.isUnset("text")) {//$NON-NLS-1$
+			// "binary" or "-text" (which is included in the binary expansion)
+			return EolStreamType.DIRECT;
+		}
+
 		// old git system
 		if (attrs.isSet("crlf")) {//$NON-NLS-1$
 			return FORCE_EOL_LF_ON_CHECKOUT ? EolStreamType.TEXT_LF
@@ -194,9 +201,6 @@
 		}
 
 		// new git system
-		if (attrs.isUnset("text")) {//$NON-NLS-1$
-			return EolStreamType.DIRECT;
-		}
 		String eol = attrs.getValue("eol"); //$NON-NLS-1$
 		if (eol != null && "crlf".equals(eol)) //$NON-NLS-1$
 			return EolStreamType.TEXT_CRLF;