Merge "Update smudged entries when writing index" into stable-2.0
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
index b1cac3a..a51a8b4 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
@@ -61,6 +61,8 @@
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
 import org.eclipse.jgit.api.errors.RefNotFoundException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -236,4 +238,41 @@ public void testDetachedHeadOnCheckout() throws JGitInternalException,
 		assertFalse(head.isSymbolic());
 		assertSame(head, head.getTarget());
 	}
+
+	@Test
+	public void testUpdateSmudgedEntries() throws Exception {
+		git.branchCreate().setName("test2").call();
+		RefUpdate rup = db.updateRef(Constants.HEAD);
+		rup.link("refs/heads/test2");
+
+		File file = new File(db.getWorkTree(), "Test.txt");
+		long size = file.length();
+		long mTime = file.lastModified() - 5000L;
+		assertTrue(file.setLastModified(mTime));
+
+		DirCache cache = DirCache.lock(db.getIndexFile(), db.getFS());
+		DirCacheEntry entry = cache.getEntry("Test.txt");
+		assertNotNull(entry);
+		entry.setLength(0);
+		entry.setLastModified(0);
+		cache.write();
+		assertTrue(cache.commit());
+
+		cache = DirCache.read(db.getIndexFile(), db.getFS());
+		entry = cache.getEntry("Test.txt");
+		assertNotNull(entry);
+		assertEquals(0, entry.getLength());
+		assertEquals(0, entry.getLastModified());
+
+		db.getIndexFile().setLastModified(
+				db.getIndexFile().lastModified() - 5000);
+
+		assertNotNull(git.checkout().setName("test").call());
+
+		cache = DirCache.read(db.getIndexFile(), db.getFS());
+		entry = cache.getEntry("Test.txt");
+		assertNotNull(entry);
+		assertEquals(size, entry.getLength());
+		assertEquals(mTime, entry.getLastModified());
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
index b9f5882..3aec611 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
@@ -50,6 +50,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
@@ -258,4 +259,110 @@ public void commitSubmoduleUpdate() throws Exception {
 		assertEquals(path, subDiff.getNewPath());
 		assertEquals(path, subDiff.getOldPath());
 	}
+
+	@Test
+	public void commitUpdatesSmudgedEntries() throws Exception {
+		Git git = new Git(db);
+
+		File file1 = writeTrashFile("file1.txt", "content1");
+		assertTrue(file1.setLastModified(file1.lastModified() - 5000));
+		File file2 = writeTrashFile("file2.txt", "content2");
+		assertTrue(file2.setLastModified(file2.lastModified() - 5000));
+		File file3 = writeTrashFile("file3.txt", "content3");
+		assertTrue(file3.setLastModified(file3.lastModified() - 5000));
+
+		assertNotNull(git.add().addFilepattern("file1.txt")
+				.addFilepattern("file2.txt").addFilepattern("file3.txt").call());
+		RevCommit commit = git.commit().setMessage("add files").call();
+		assertNotNull(commit);
+
+		DirCache cache = DirCache.read(db.getIndexFile(), db.getFS());
+		int file1Size = cache.getEntry("file1.txt").getLength();
+		int file2Size = cache.getEntry("file2.txt").getLength();
+		int file3Size = cache.getEntry("file3.txt").getLength();
+		ObjectId file2Id = cache.getEntry("file2.txt").getObjectId();
+		ObjectId file3Id = cache.getEntry("file3.txt").getObjectId();
+		assertTrue(file1Size > 0);
+		assertTrue(file2Size > 0);
+		assertTrue(file3Size > 0);
+
+		// Smudge entries
+		cache = DirCache.lock(db.getIndexFile(), db.getFS());
+		cache.getEntry("file1.txt").setLength(0);
+		cache.getEntry("file2.txt").setLength(0);
+		cache.getEntry("file3.txt").setLength(0);
+		cache.write();
+		assertTrue(cache.commit());
+
+		// Verify entries smudged
+		cache = DirCache.read(db.getIndexFile(), db.getFS());
+		assertEquals(0, cache.getEntry("file1.txt").getLength());
+		assertEquals(0, cache.getEntry("file2.txt").getLength());
+		assertEquals(0, cache.getEntry("file3.txt").getLength());
+
+		long indexTime = db.getIndexFile().lastModified();
+		db.getIndexFile().setLastModified(indexTime - 5000);
+
+		write(file1, "content4");
+		assertTrue(file1.setLastModified(file1.lastModified() + 1000));
+		assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt")
+				.call());
+
+		cache = db.readDirCache();
+		assertEquals(file1Size, cache.getEntry("file1.txt").getLength());
+		assertEquals(file2Size, cache.getEntry("file2.txt").getLength());
+		assertEquals(file3Size, cache.getEntry("file3.txt").getLength());
+		assertEquals(file2Id, cache.getEntry("file2.txt").getObjectId());
+		assertEquals(file3Id, cache.getEntry("file3.txt").getObjectId());
+	}
+
+	@Test
+	public void commitIgnoresSmudgedEntryWithDifferentId() throws Exception {
+		Git git = new Git(db);
+
+		File file1 = writeTrashFile("file1.txt", "content1");
+		assertTrue(file1.setLastModified(file1.lastModified() - 5000));
+		File file2 = writeTrashFile("file2.txt", "content2");
+		assertTrue(file2.setLastModified(file2.lastModified() - 5000));
+
+		assertNotNull(git.add().addFilepattern("file1.txt")
+				.addFilepattern("file2.txt").call());
+		RevCommit commit = git.commit().setMessage("add files").call();
+		assertNotNull(commit);
+
+		DirCache cache = DirCache.read(db.getIndexFile(), db.getFS());
+		int file1Size = cache.getEntry("file1.txt").getLength();
+		int file2Size = cache.getEntry("file2.txt").getLength();
+		assertTrue(file1Size > 0);
+		assertTrue(file2Size > 0);
+
+		writeTrashFile("file2.txt", "content3");
+		assertNotNull(git.add().addFilepattern("file2.txt").call());
+		writeTrashFile("file2.txt", "content4");
+
+		// Smudge entries
+		cache = DirCache.lock(db.getIndexFile(), db.getFS());
+		cache.getEntry("file1.txt").setLength(0);
+		cache.getEntry("file2.txt").setLength(0);
+		cache.write();
+		assertTrue(cache.commit());
+
+		// Verify entries smudged
+		cache = db.readDirCache();
+		assertEquals(0, cache.getEntry("file1.txt").getLength());
+		assertEquals(0, cache.getEntry("file2.txt").getLength());
+
+		long indexTime = db.getIndexFile().lastModified();
+		db.getIndexFile().setLastModified(indexTime - 5000);
+
+		write(file1, "content5");
+		assertTrue(file1.setLastModified(file1.lastModified() + 1000));
+
+		assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt")
+				.call());
+
+		cache = db.readDirCache();
+		assertEquals(file1Size, cache.getEntry("file1.txt").getLength());
+		assertEquals(0, cache.getEntry("file2.txt").getLength());
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
index 27c3549..3087ca8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
@@ -220,6 +220,48 @@ public void testMixedReset() throws JGitInternalException,
 	}
 
 	@Test
+	public void testMixedResetRetainsSizeAndModifiedTime() throws Exception {
+		git = new Git(db);
+
+		writeTrashFile("a.txt", "a").setLastModified(
+				System.currentTimeMillis() - 60 * 1000);
+		assertNotNull(git.add().addFilepattern("a.txt").call());
+		assertNotNull(git.commit().setMessage("a commit").call());
+
+		writeTrashFile("b.txt", "b").setLastModified(
+				System.currentTimeMillis() - 60 * 1000);
+		assertNotNull(git.add().addFilepattern("b.txt").call());
+		RevCommit commit2 = git.commit().setMessage("b commit").call();
+		assertNotNull(commit2);
+
+		DirCache cache = db.readDirCache();
+
+		DirCacheEntry aEntry = cache.getEntry("a.txt");
+		assertNotNull(aEntry);
+		assertTrue(aEntry.getLength() > 0);
+		assertTrue(aEntry.getLastModified() > 0);
+
+		DirCacheEntry bEntry = cache.getEntry("b.txt");
+		assertNotNull(bEntry);
+		assertTrue(bEntry.getLength() > 0);
+		assertTrue(bEntry.getLastModified() > 0);
+
+		git.reset().setMode(ResetType.MIXED).setRef(commit2.getName()).call();
+
+		cache = db.readDirCache();
+
+		DirCacheEntry mixedAEntry = cache.getEntry("a.txt");
+		assertNotNull(mixedAEntry);
+		assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified());
+		assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified());
+
+		DirCacheEntry mixedBEntry = cache.getEntry("b.txt");
+		assertNotNull(mixedBEntry);
+		assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified());
+		assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified());
+	}
+
+	@Test
 	public void testPathsReset() throws Exception {
 		setupRepository();
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
index 0d5952d..9108d92 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -69,8 +69,11 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.storage.file.FileSnapshot;
 import org.eclipse.jgit.storage.file.LockFile;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.MutableInteger;
@@ -145,6 +148,30 @@ public static DirCache newInCore() {
 	 * failures are reported as exceptions and therefore prevent the method from
 	 * returning a partially populated index.
 	 *
+	 * @param repository
+	 *            repository containing the index to read
+	 * @return a cache representing the contents of the specified index file (if
+	 *         it exists) or an empty cache if the file does not exist.
+	 * @throws IOException
+	 *             the index file is present but could not be read.
+	 * @throws CorruptObjectException
+	 *             the index file is using a format or extension that this
+	 *             library does not support.
+	 */
+	public static DirCache read(final Repository repository)
+			throws CorruptObjectException, IOException {
+		final DirCache c = read(repository.getIndexFile(), repository.getFS());
+		c.repository = repository;
+		return c;
+	}
+
+	/**
+	 * Create a new in-core index representation and read an index from disk.
+	 * <p>
+	 * The new index will be read before it is returned to the caller. Read
+	 * failures are reported as exceptions and therefore prevent the method from
+	 * returning a partially populated index.
+	 *
 	 * @param indexLocation
 	 *            location of the index file on disk.
 	 * @param fs
@@ -217,6 +244,37 @@ public static DirCache lock(final File indexLocation, final FS fs)
 	 * the method from returning a partially populated index. On read failure,
 	 * the lock is released.
 	 *
+	 * @param repository
+	 *            repository containing the index to lock and read
+	 * @param indexChangedListener
+	 *            listener to be informed when DirCache is committed
+	 * @return a cache representing the contents of the specified index file (if
+	 *         it exists) or an empty cache if the file does not exist.
+	 * @throws IOException
+	 *             the index file is present but could not be read, or the lock
+	 *             could not be obtained.
+	 * @throws CorruptObjectException
+	 *             the index file is using a format or extension that this
+	 *             library does not support.
+	 * @since 2.0
+	 */
+	public static DirCache lock(final Repository repository,
+			final IndexChangedListener indexChangedListener)
+			throws CorruptObjectException, IOException {
+		DirCache c = lock(repository.getIndexFile(), repository.getFS(),
+				indexChangedListener);
+		c.repository = repository;
+		return c;
+	}
+
+	/**
+	 * Create a new in-core index representation, lock it, and read from disk.
+	 * <p>
+	 * The new index will be locked and then read before it is returned to the
+	 * caller. Read failures are reported as exceptions and therefore prevent
+	 * the method from returning a partially populated index. On read failure,
+	 * the lock is released.
+	 *
 	 * @param indexLocation
 	 *            location of the index file on disk.
 	 * @param fs
@@ -272,6 +330,9 @@ public static DirCache lock(final File indexLocation, final FS fs,
 	/** listener to be informed on commit */
 	private IndexChangedListener indexChangedListener;
 
+	/** Repository containing this index */
+	private Repository repository;
+
 	/**
 	 * Create a new in-core index representation.
 	 * <p>
@@ -591,6 +652,13 @@ void writeTo(final OutputStream os) throws IOException {
 			smudge_s = 0;
 		}
 
+		// Check if tree is non-null here since calling updateSmudgedEntries
+		// will automatically build it via creating a DirCacheIterator
+		final boolean writeTree = tree != null;
+
+		if (repository != null && entryCnt > 0)
+			updateSmudgedEntries();
+
 		for (int i = 0; i < entryCnt; i++) {
 			final DirCacheEntry e = sortedEntries[i];
 			if (e.mightBeRacilyClean(smudge_s, smudge_ns))
@@ -598,7 +666,7 @@ void writeTo(final OutputStream os) throws IOException {
 			e.write(dos);
 		}
 
-		if (tree != null) {
+		if (writeTree) {
 			final TemporaryBuffer bb = new TemporaryBuffer.LocalFile();
 			tree.write(tmp, bb);
 			bb.close();
@@ -865,4 +933,35 @@ public boolean hasUnmergedPaths() {
 	private void registerIndexChangedListener(IndexChangedListener listener) {
 		this.indexChangedListener = listener;
 	}
+
+	/**
+	 * Update any smudged entries with information from the working tree.
+	 *
+	 * @throws IOException
+	 */
+	private void updateSmudgedEntries() throws IOException {
+		TreeWalk walk = new TreeWalk(repository);
+		try {
+			DirCacheIterator iIter = new DirCacheIterator(this);
+			FileTreeIterator fIter = new FileTreeIterator(repository);
+			walk.addTree(iIter);
+			walk.addTree(fIter);
+			walk.setRecursive(true);
+			while (walk.next()) {
+				iIter = walk.getTree(0, DirCacheIterator.class);
+				if (iIter == null)
+					continue;
+				fIter = walk.getTree(1, FileTreeIterator.class);
+				if (fIter == null)
+					continue;
+				DirCacheEntry entry = iIter.getDirCacheEntry();
+				if (entry.isSmudged() && iIter.idEqual(fIter)) {
+					entry.setLength(fIter.getEntryLength());
+					entry.setLastModified(fIter.getEntryLastModified());
+				}
+			}
+		} finally {
+			walk.release();
+		}
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index c70c9b0..5d9488a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -871,7 +871,7 @@ public File getIndexFile() throws NoWorkTreeException {
 	 */
 	public DirCache readDirCache() throws NoWorkTreeException,
 			CorruptObjectException, IOException {
-		return DirCache.read(getIndexFile(), getFS());
+		return DirCache.read(this);
 	}
 
 	/**
@@ -903,7 +903,7 @@ public void onIndexChanged(IndexChangedEvent event) {
 				notifyIndexChanged();
 			}
 		};
-		return DirCache.lock(getIndexFile(), getFS(), l);
+		return DirCache.lock(this, l);
 	}
 
 	static byte[] gitInternalSlash(byte[] bytes) {