DirCache: support index V4

Index format version 4 was introduced in C git in 2012. It's about
time that JGit can deal with it.

Version 4 added prefix path compression. Instead of writing the full
path for each index entry to disk, only the difference to the previous
entry's path is written: a variable-encoded int telling how many bytes
to remove from the previous entry's path to get the common prefix,
followed by the new suffix.

Also, cache entries in a version 4 index are not padded anymore.

Internally, version 3 and version 4 index entries are identical; it's
only the stored format that changes.

Implement this path compression, and make sure we write an index file
that we read previously in the same format. (Only changing from version
2 to version 3 if there are extended flags.)

Add support for the "feature.manyFiles" and the "index.version" git
configs, and honor them when writing a new index file.

Add tests, including a compatibility test that verifies that JGit can
read a version 4 index generated by C git and write an identical
version 4 index.

Bug: 565774
Change-Id: Id83241cf009e50f950eb42f8d56b834fb47da1ed
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/gitgit.index.v4 b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/gitgit.index.v4
new file mode 100644
index 0000000..de49e56
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/gitgit.index.v4
Binary files differ
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheAfterCloneTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheAfterCloneTest.java
new file mode 100644
index 0000000..f210760
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheAfterCloneTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.dircache;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.dircache.DirCache.DirCacheVersion;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.Test;
+
+/**
+ * Tests for initial DirCache version after a clone or after a mixed or hard
+ * reset.
+ */
+public class DirCacheAfterCloneTest extends RepositoryTestCase {
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		try (Git git = new Git(db)) {
+			writeTrashFile("Test.txt", "Hello world");
+			git.add().addFilepattern("Test.txt").call();
+			git.commit().setMessage("Initial commit").call();
+		}
+	}
+
+	private DirCacheVersion cloneAndCheck(Set<DirCacheVersion> expected)
+			throws Exception {
+		File directory = createTempDirectory("testCloneRepository");
+		CloneCommand command = Git.cloneRepository();
+		command.setDirectory(directory);
+		command.setURI("file://" + db.getWorkTree().getAbsolutePath());
+		Git git2 = command.call();
+		addRepoToClose(git2.getRepository());
+		assertNotNull(git2);
+		DirCache dc = DirCache.read(git2.getRepository());
+		DirCacheVersion version = dc.getVersion();
+		assertTrue(expected.contains(version));
+		return version;
+	}
+
+	@Test
+	public void testCloneV3OrV2() throws Exception {
+		cloneAndCheck(EnumSet.of(DirCacheVersion.DIRC_VERSION_MINIMUM,
+				DirCacheVersion.DIRC_VERSION_EXTENDED));
+	}
+
+	@Test
+	public void testCloneV4() throws Exception {
+		StoredConfig cfg = SystemReader.getInstance().getUserConfig();
+		cfg.load();
+		cfg.setInt("index", null, "version", 4);
+		cfg.save();
+		cloneAndCheck(EnumSet.of(DirCacheVersion.DIRC_VERSION_PATHCOMPRESS));
+	}
+
+	@Test
+	public void testCloneV4manyFiles() throws Exception {
+		StoredConfig cfg = SystemReader.getInstance().getUserConfig();
+		cfg.load();
+		cfg.setBoolean("feature", null, "manyFiles", true);
+		cfg.save();
+		cloneAndCheck(EnumSet.of(DirCacheVersion.DIRC_VERSION_PATHCOMPRESS));
+	}
+
+	@Test
+	public void testCloneV3CommitNoVersionChange() throws Exception {
+		DirCacheVersion initial = cloneAndCheck(
+				EnumSet.of(DirCacheVersion.DIRC_VERSION_MINIMUM,
+						DirCacheVersion.DIRC_VERSION_EXTENDED));
+		StoredConfig cfg = db.getConfig();
+		cfg.setInt("index", null, "version", 4);
+		cfg.save();
+		try (Git git = new Git(db)) {
+			writeTrashFile("Test.txt2", "Hello again");
+			git.add().addFilepattern("Test.txt2").call();
+			git.commit().setMessage("Second commit").call();
+		}
+		assertEquals("DirCache version should be unchanged", initial,
+				DirCache.read(db).getVersion());
+	}
+
+	@Test
+	public void testCloneV3ResetHardVersionChange() throws Exception {
+		cloneAndCheck(EnumSet.of(DirCacheVersion.DIRC_VERSION_MINIMUM,
+						DirCacheVersion.DIRC_VERSION_EXTENDED));
+		StoredConfig cfg = db.getConfig();
+		cfg.setInt("index", null, "version", 4);
+		cfg.save();
+		FileUtils.delete(new File(db.getDirectory(), "index"));
+		try (Git git = new Git(db)) {
+			git.reset().setMode(ResetType.HARD).call();
+		}
+		assertEquals("DirCache version should have changed",
+				DirCacheVersion.DIRC_VERSION_PATHCOMPRESS,
+				DirCache.read(db).getVersion());
+	}
+
+	@Test
+	public void testCloneV3ResetMixedVersionChange() throws Exception {
+		cloneAndCheck(EnumSet.of(DirCacheVersion.DIRC_VERSION_MINIMUM,
+				DirCacheVersion.DIRC_VERSION_EXTENDED));
+		StoredConfig cfg = db.getConfig();
+		cfg.setInt("index", null, "version", 4);
+		cfg.save();
+		FileUtils.delete(new File(db.getDirectory(), "index"));
+		try (Git git = new Git(db)) {
+			git.reset().setMode(ResetType.MIXED).call();
+		}
+		assertEquals("DirCache version should have changed",
+				DirCacheVersion.DIRC_VERSION_PATHCOMPRESS,
+				DirCache.read(db).getVersion());
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheCGitCompatabilityTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheCGitCompatabilityTest.java
index c57cb26..6d4d0b4 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheCGitCompatabilityTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheCGitCompatabilityTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2010, Google Inc. and others
+ * Copyright (C) 2008, 2020, Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -28,6 +28,7 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import org.eclipse.jgit.dircache.DirCache.DirCacheVersion;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
@@ -188,6 +189,28 @@
 		assertArrayEquals(expectedBytes, indexBytes);
 	}
 
+	@Test
+	public void testReadWriteV4() throws Exception {
+		final File file = pathOf("gitgit.index.v4");
+		final DirCache dc = new DirCache(file, FS.DETECTED);
+		dc.read();
+		assertEquals(DirCacheVersion.DIRC_VERSION_PATHCOMPRESS,
+				dc.getVersion());
+		assertEquals(5, dc.getEntryCount());
+		assertV4TreeEntry(0, "src/org/eclipse/jgit/atest/foo.txt", false, dc);
+		assertV4TreeEntry(1, "src/org/eclipse/jgit/atest/foobar.txt", false,
+				dc);
+		assertV4TreeEntry(2, "src/org/eclipse/jgit/other/bar.txt", true, dc);
+		assertV4TreeEntry(3, "test.txt", false, dc);
+		assertV4TreeEntry(4, "test.txt2", false, dc);
+
+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+		dc.writeTo(null, bos);
+		final byte[] indexBytes = bos.toByteArray();
+		final byte[] expectedBytes = IO.readFully(file);
+		assertArrayEquals(expectedBytes, indexBytes);
+	}
+
 	private static void assertV3TreeEntry(int indexPosition, String path,
 			boolean skipWorkTree, boolean intentToAdd, DirCache dc) {
 		final DirCacheEntry entry = dc.getEntry(indexPosition);
@@ -196,6 +219,13 @@
 		assertEquals(intentToAdd, entry.isIntentToAdd());
 	}
 
+	private static void assertV4TreeEntry(int indexPosition, String path,
+			boolean skipWorkTree, DirCache dc) {
+		final DirCacheEntry entry = dc.getEntry(indexPosition);
+		assertEquals(path, entry.getPathString());
+		assertEquals(skipWorkTree, entry.isSkipWorkTree());
+	}
+
 	private static File pathOf(String name) {
 		return JGitTestUtil.getTestResourceFile(name);
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
index f21c7f8..5477f56 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009, Google Inc. and others
+ * Copyright (C) 2009, 2020 Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,14 +11,25 @@
 package org.eclipse.jgit.dircache;
 
 import static java.time.Instant.EPOCH;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.dircache.DirCache.DirCacheVersion;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.MutableInteger;
 import org.junit.Test;
 
 public class DirCacheEntryTest {
@@ -47,6 +58,95 @@
 		}
 	}
 
+	private static void checkPath(DirCacheVersion indexVersion,
+			DirCacheEntry previous, String name) throws IOException {
+		DirCacheEntry dce = new DirCacheEntry(name);
+		long now = System.currentTimeMillis();
+		long anHourAgo = now - TimeUnit.HOURS.toMillis(1);
+		dce.setLastModified(Instant.ofEpochMilli(anHourAgo));
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		dce.write(out, indexVersion, previous);
+		byte[] raw = out.toByteArray();
+		MessageDigest md0 = Constants.newMessageDigest();
+		md0.update(raw);
+		ByteArrayInputStream in = new ByteArrayInputStream(raw);
+		MutableInteger infoAt = new MutableInteger();
+		byte[] sharedInfo = new byte[raw.length];
+		MessageDigest md = Constants.newMessageDigest();
+		DirCacheEntry read = new DirCacheEntry(sharedInfo, infoAt, in, md,
+				Instant.ofEpochMilli(now), indexVersion, previous);
+		assertEquals("Paths of length " + name.length() + " should match", name,
+				read.getPathString());
+		assertEquals("Should have been fully read", -1, in.read());
+		assertArrayEquals("Digests should match", md0.digest(),
+				md.digest());
+	}
+
+	@Test
+	public void testLongPath() throws Exception {
+		StringBuilder name = new StringBuilder(4094 + 16);
+		for (int i = 0; i < 4094; i++) {
+			name.append('a');
+		}
+		for (int j = 0; j < 16; j++) {
+			checkPath(DirCacheVersion.DIRC_VERSION_EXTENDED, null,
+					name.toString());
+			name.append('b');
+		}
+	}
+
+	@Test
+	public void testLongPathV4() throws Exception {
+		StringBuilder name = new StringBuilder(4094 + 16);
+		for (int i = 0; i < 4094; i++) {
+			name.append('a');
+		}
+		DirCacheEntry previous = new DirCacheEntry(name.toString());
+		for (int j = 0; j < 16; j++) {
+			checkPath(DirCacheVersion.DIRC_VERSION_PATHCOMPRESS, previous,
+					name.toString());
+			name.append('b');
+		}
+	}
+
+	@Test
+	public void testShortPath() throws Exception {
+		StringBuilder name = new StringBuilder(1 + 16);
+		name.append('a');
+		for (int j = 0; j < 16; j++) {
+			checkPath(DirCacheVersion.DIRC_VERSION_EXTENDED, null,
+					name.toString());
+			name.append('b');
+		}
+	}
+
+	@Test
+	public void testShortPathV4() throws Exception {
+		StringBuilder name = new StringBuilder(1 + 16);
+		name.append('a');
+		DirCacheEntry previous = new DirCacheEntry(name.toString());
+		for (int j = 0; j < 16; j++) {
+			checkPath(DirCacheVersion.DIRC_VERSION_PATHCOMPRESS, previous,
+					name.toString());
+			name.append('b');
+		}
+	}
+
+	@Test
+	public void testPathV4() throws Exception {
+		StringBuilder name = new StringBuilder();
+		for (int i = 0; i < 20; i++) {
+			name.append('a');
+		}
+		DirCacheEntry previous = new DirCacheEntry(name.toString());
+		for (int j = 0; j < 20; j++) {
+			name.setLength(name.length() - 1);
+			String newName = name.toString() + "bbb";
+			checkPath(DirCacheVersion.DIRC_VERSION_PATHCOMPRESS, previous,
+					newName);
+		}
+	}
+
 	@SuppressWarnings("unused")
 	@Test
 	public void testCreate_ByStringPath() {
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 899a799..7088335 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -224,6 +224,8 @@
 dirCacheFileIsNotLocked=DirCache {0} not locked
 dirCacheIsNotLocked=DirCache is not locked
 DIRCChecksumMismatch=DIRC checksum mismatch
+DIRCCorruptLength=DIRC variable int {0} invalid after entry for {1}
+DIRCCorruptLengthFirst=DIRC variable int {0} invalid in first entry
 DIRCExtensionIsTooLargeAt=DIRC extension {0} is too large at {1} bytes.
 DIRCExtensionNotSupportedByThisVersion=DIRC extension {0} not supported by this version.
 DIRCHasTooManyEntries=DIRC has too many entries.
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 b2764d7..bb725b7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010, Google Inc.
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
- * Copyright (C) 2011, Matthias Sohn <matthias.sohn@sap.com> and others
+ * Copyright (C) 2011, 2020, Matthias Sohn <matthias.sohn@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -41,6 +41,9 @@
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.ConfigEnum;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -321,6 +324,9 @@
 	/** Repository containing this index */
 	private Repository repository;
 
+	/** If we read this index from disk, the original format. */
+	private DirCacheVersion version;
+
 	/**
 	 * Create a new in-core index representation.
 	 * <p>
@@ -364,6 +370,10 @@
 		return new DirCacheEditor(this, entryCnt + 16);
 	}
 
+	DirCacheVersion getVersion() {
+		return version;
+	}
+
 	void replace(DirCacheEntry[] e, int cnt) {
 		sortedEntries = e;
 		entryCnt = cnt;
@@ -445,13 +455,26 @@
 		md.update(hdr, 0, 12);
 		if (!is_DIRC(hdr))
 			throw new CorruptObjectException(JGitText.get().notADIRCFile);
-		final int ver = NB.decodeInt32(hdr, 4);
+		int versionCode = NB.decodeInt32(hdr, 4);
+		DirCacheVersion ver = DirCacheVersion.fromInt(versionCode);
+		if (ver == null) {
+			throw new CorruptObjectException(
+					MessageFormat.format(JGitText.get().unknownDIRCVersion,
+							Integer.valueOf(versionCode)));
+		}
 		boolean extended = false;
-		if (ver == 3)
+		switch (ver) {
+		case DIRC_VERSION_MINIMUM:
+			break;
+		case DIRC_VERSION_EXTENDED:
+		case DIRC_VERSION_PATHCOMPRESS:
 			extended = true;
-		else if (ver != 2)
-			throw new CorruptObjectException(MessageFormat.format(
-					JGitText.get().unknownDIRCVersion, Integer.valueOf(ver)));
+			break;
+		default:
+			throw new CorruptObjectException(MessageFormat
+					.format(JGitText.get().unknownDIRCVersion, ver));
+		}
+		version = ver;
 		entryCnt = NB.decodeInt32(hdr, 8);
 		if (entryCnt < 0)
 			throw new CorruptObjectException(JGitText.get().DIRCHasTooManyEntries);
@@ -467,7 +490,8 @@
 
 		final MutableInteger infoAt = new MutableInteger();
 		for (int i = 0; i < entryCnt; i++) {
-			sortedEntries[i] = new DirCacheEntry(infos, infoAt, in, md, smudge);
+			sortedEntries[i] = new DirCacheEntry(infos, infoAt, in, md, smudge,
+					version, i == 0 ? null : sortedEntries[i - 1]);
 		}
 
 		// After the file entries are index extensions, and then a footer.
@@ -606,11 +630,20 @@
 		final MessageDigest foot = Constants.newMessageDigest();
 		final DigestOutputStream dos = new DigestOutputStream(os, foot);
 
-		boolean extended = false;
-		for (int i = 0; i < entryCnt; i++) {
-			if (sortedEntries[i].isExtended()) {
-				extended = true;
-				break;
+		if (version == null && this.repository != null) {
+			// A new DirCache is being written.
+			DirCacheConfig config = repository.getConfig()
+					.get(DirCacheConfig::new);
+			version = config.getIndexVersion();
+		}
+		if (version == null
+				|| version == DirCacheVersion.DIRC_VERSION_MINIMUM) {
+			version = DirCacheVersion.DIRC_VERSION_MINIMUM;
+			for (int i = 0; i < entryCnt; i++) {
+				if (sortedEntries[i].isExtended()) {
+					version = DirCacheVersion.DIRC_VERSION_EXTENDED;
+					break;
+				}
 			}
 		}
 
@@ -618,7 +651,7 @@
 		//
 		final byte[] tmp = new byte[128];
 		System.arraycopy(SIG_DIRC, 0, tmp, 0, SIG_DIRC.length);
-		NB.encodeInt32(tmp, 4, extended ? 3 : 2);
+		NB.encodeInt32(tmp, 4, version.getVersionCode());
 		NB.encodeInt32(tmp, 8, entryCnt);
 		dos.write(tmp, 0, 12);
 
@@ -650,7 +683,7 @@
 			if (e.mightBeRacilyClean(smudge)) {
 				e.smudgeRacilyClean();
 			}
-			e.write(dos);
+			e.write(dos, version, i == 0 ? null : sortedEntries[i - 1]);
 		}
 
 		if (writeTree) {
@@ -982,4 +1015,76 @@
 			}
 		}
 	}
+
+	enum DirCacheVersion implements ConfigEnum {
+
+		/** Minimum index version on-disk format that we support. */
+		DIRC_VERSION_MINIMUM(2),
+		/** Version 3 supports extended flags. */
+		DIRC_VERSION_EXTENDED(3),
+		/**
+		 * Version 4 adds very simple "path compression": it strips out the
+		 * common prefix between the last entry written and the current entry.
+		 * Instead of writing two entries with paths "foo/bar/baz/a.txt" and
+		 * "foo/bar/baz/b.txt" it only writes "b.txt" for the second entry.
+		 * <p>
+		 * It is also <em>not</em> padded.
+		 * </p>
+		 */
+		DIRC_VERSION_PATHCOMPRESS(4);
+
+		private int version;
+
+		private DirCacheVersion(int versionCode) {
+			this.version = versionCode;
+		}
+
+		public int getVersionCode() {
+			return version;
+		}
+
+		@Override
+		public String toConfigValue() {
+			return Integer.toString(version);
+		}
+
+		@Override
+		public boolean matchConfigValue(String in) {
+			try {
+				return version == Integer.parseInt(in);
+			} catch (NumberFormatException e) {
+				return false;
+			}
+		}
+
+		public static DirCacheVersion fromInt(int val) {
+			for (DirCacheVersion v : DirCacheVersion.values()) {
+				if (val == v.getVersionCode()) {
+					return v;
+				}
+			}
+			return null;
+		}
+	}
+
+	private static class DirCacheConfig {
+
+		private final DirCacheVersion indexVersion;
+
+		public DirCacheConfig(Config cfg) {
+			boolean manyFiles = cfg.getBoolean(
+					ConfigConstants.CONFIG_FEATURE_SECTION,
+					ConfigConstants.CONFIG_KEY_MANYFILES, false);
+			indexVersion = cfg.getEnum(DirCacheVersion.values(),
+					ConfigConstants.CONFIG_INDEX_SECTION, null,
+					ConfigConstants.CONFIG_KEY_VERSION,
+					manyFiles ? DirCacheVersion.DIRC_VERSION_PATHCOMPRESS
+							: DirCacheVersion.DIRC_VERSION_EXTENDED);
+		}
+
+		public DirCacheVersion getIndexVersion() {
+			return indexVersion;
+		}
+
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
index ced379f..e7d62c7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
+ * Copyright (C) 2008, 2009, Google Inc.
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2010, Matthias Sohn <matthias.sohn@sap.com>
- * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2010, 2020, Christian Halstrick <christian.halstrick@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -26,6 +26,7 @@
 import java.time.Instant;
 import java.util.Arrays;
 
+import org.eclipse.jgit.dircache.DirCache.DirCacheVersion;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -112,15 +113,16 @@
 	/** Flags which are never stored to disk. */
 	private byte inCoreFlags;
 
-	DirCacheEntry(final byte[] sharedInfo, final MutableInteger infoAt,
-			final InputStream in, final MessageDigest md, final Instant smudge)
+	DirCacheEntry(byte[] sharedInfo, MutableInteger infoAt, InputStream in,
+			MessageDigest md, Instant smudge, DirCacheVersion version,
+			DirCacheEntry previous)
 			throws IOException {
 		info = sharedInfo;
 		infoOffset = infoAt.value;
 
 		IO.readFully(in, info, infoOffset, INFO_LEN);
 
-		final int len;
+		int len;
 		if (isExtended()) {
 			len = INFO_LEN_EXTENDED;
 			IO.readFully(in, info, infoOffset + INFO_LEN, INFO_LEN_EXTENDED - INFO_LEN);
@@ -134,31 +136,66 @@
 		infoAt.value += len;
 		md.update(info, infoOffset, len);
 
+		int toRemove = 0;
+		if (version == DirCacheVersion.DIRC_VERSION_PATHCOMPRESS) {
+			// Read variable int and update digest
+			int b = in.read();
+			md.update((byte) b);
+			toRemove = b & 0x7F;
+			while ((b & 0x80) != 0) {
+				toRemove++;
+				b = in.read();
+				md.update((byte) b);
+				toRemove = (toRemove << 7) | (b & 0x7F);
+			}
+			if (toRemove < 0
+					|| previous != null && toRemove > previous.path.length) {
+				if (previous == null) {
+					throw new IOException(MessageFormat.format(
+							JGitText.get().DIRCCorruptLengthFirst,
+							Integer.valueOf(toRemove)));
+				}
+				throw new IOException(MessageFormat.format(
+						JGitText.get().DIRCCorruptLength,
+						Integer.valueOf(toRemove), previous.getPathString()));
+			}
+		}
 		int pathLen = NB.decodeUInt16(info, infoOffset + P_FLAGS) & NAME_MASK;
 		int skipped = 0;
 		if (pathLen < NAME_MASK) {
 			path = new byte[pathLen];
-			IO.readFully(in, path, 0, pathLen);
-			md.update(path, 0, pathLen);
-		} else {
-			final ByteArrayOutputStream tmp = new ByteArrayOutputStream();
-			{
-				final byte[] buf = new byte[NAME_MASK];
-				IO.readFully(in, buf, 0, NAME_MASK);
-				tmp.write(buf);
+			if (version == DirCacheVersion.DIRC_VERSION_PATHCOMPRESS
+					&& previous != null) {
+				System.arraycopy(previous.path, 0, path, 0,
+						previous.path.length - toRemove);
+				IO.readFully(in, path, previous.path.length - toRemove,
+						pathLen - (previous.path.length - toRemove));
+				md.update(path, previous.path.length - toRemove,
+						pathLen - (previous.path.length - toRemove));
+				pathLen = pathLen - (previous.path.length - toRemove);
+			} else {
+				IO.readFully(in, path, 0, pathLen);
+				md.update(path, 0, pathLen);
 			}
-			for (;;) {
-				final int c = in.read();
-				if (c < 0)
-					throw new EOFException(JGitText.get().shortReadOfBlock);
-				if (c == 0)
-					break;
-				tmp.write(c);
-			}
+		} else if (version != DirCacheVersion.DIRC_VERSION_PATHCOMPRESS
+				|| previous == null || toRemove == previous.path.length) {
+			ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+			byte[] buf = new byte[NAME_MASK];
+			IO.readFully(in, buf, 0, NAME_MASK);
+			tmp.write(buf);
+			readNulTerminatedString(in, tmp);
 			path = tmp.toByteArray();
 			pathLen = path.length;
-			skipped = 1; // we already skipped 1 '\0' above to break the loop.
 			md.update(path, 0, pathLen);
+			skipped = 1; // we already skipped 1 '\0' in readNulTerminatedString
+			md.update((byte) 0);
+		} else {
+			ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+			tmp.write(previous.path, 0, previous.path.length - toRemove);
+			pathLen = readNulTerminatedString(in, tmp);
+			path = tmp.toByteArray();
+			md.update(path, previous.path.length - toRemove, pathLen);
+			skipped = 1; // we already skipped 1 '\0' in readNulTerminatedString
 			md.update((byte) 0);
 		}
 
@@ -172,17 +209,26 @@
 			throw p;
 		}
 
-		// Index records are padded out to the next 8 byte alignment
-		// for historical reasons related to how C Git read the files.
-		//
-		final int actLen = len + pathLen;
-		final int expLen = (actLen + 8) & ~7;
-		final int padLen = expLen - actLen - skipped;
-		if (padLen > 0) {
-			IO.skipFully(in, padLen);
-			md.update(nullpad, 0, padLen);
+		if (version == DirCacheVersion.DIRC_VERSION_PATHCOMPRESS) {
+			if (skipped == 0) {
+				int b = in.read();
+				if (b < 0) {
+					throw new EOFException(JGitText.get().shortReadOfBlock);
+				}
+				md.update((byte) b);
+			}
+		} else {
+			// Index records are padded out to the next 8 byte alignment
+			// for historical reasons related to how C Git read the files.
+			//
+			final int actLen = len + pathLen;
+			final int expLen = (actLen + 8) & ~7;
+			final int padLen = expLen - actLen - skipped;
+			if (padLen > 0) {
+				IO.skipFully(in, padLen);
+				md.update(nullpad, 0, padLen);
+			}
 		}
-
 		if (mightBeRacilyClean(smudge)) {
 			smudgeRacilyClean();
 		}
@@ -283,19 +329,61 @@
 		System.arraycopy(src.info, src.infoOffset, info, 0, INFO_LEN);
 	}
 
-	void write(OutputStream os) throws IOException {
-		final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN;
-		final int pathLen = path.length;
-		os.write(info, infoOffset, len);
-		os.write(path, 0, pathLen);
+	private int readNulTerminatedString(InputStream in, OutputStream out)
+			throws IOException {
+		int n = 0;
+		for (;;) {
+			int c = in.read();
+			if (c < 0) {
+				throw new EOFException(JGitText.get().shortReadOfBlock);
+			}
+			if (c == 0) {
+				break;
+			}
+			out.write(c);
+			n++;
+		}
+		return n;
+	}
 
-		// Index records are padded out to the next 8 byte alignment
-		// for historical reasons related to how C Git read the files.
-		//
-		final int actLen = len + pathLen;
-		final int expLen = (actLen + 8) & ~7;
-		if (actLen != expLen)
-			os.write(nullpad, 0, expLen - actLen);
+	void write(OutputStream os, DirCacheVersion version, DirCacheEntry previous)
+			throws IOException {
+		final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN;
+		if (version != DirCacheVersion.DIRC_VERSION_PATHCOMPRESS) {
+			os.write(info, infoOffset, len);
+			os.write(path, 0, path.length);
+			// Index records are padded out to the next 8 byte alignment
+			// for historical reasons related to how C Git read the files.
+			//
+			int entryLen = len + path.length;
+			int expLen = (entryLen + 8) & ~7;
+			if (entryLen != expLen)
+				os.write(nullpad, 0, expLen - entryLen);
+		} else {
+			int pathCommon = 0;
+			int toRemove;
+			if (previous != null) {
+				// Figure out common prefix
+				int pathLen = Math.min(path.length, previous.path.length);
+				while (pathCommon < pathLen
+						&& path[pathCommon] == previous.path[pathCommon]) {
+					pathCommon++;
+				}
+				toRemove = previous.path.length - pathCommon;
+			} else {
+				toRemove = 0;
+			}
+			byte[] tmp = new byte[16];
+			int n = tmp.length;
+			tmp[--n] = (byte) (toRemove & 0x7F);
+			while ((toRemove >>>= 7) != 0) {
+				tmp[--n] = (byte) (0x80 | (--toRemove & 0x7F));
+			}
+			os.write(info, infoOffset, len);
+			os.write(tmp, n, tmp.length - n);
+			os.write(path, pathCommon, path.length - pathCommon);
+			os.write(0);
+		}
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 2976ab6..0e35a99 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -252,6 +252,8 @@
 	/***/ public String dirCacheFileIsNotLocked;
 	/***/ public String dirCacheIsNotLocked;
 	/***/ public String DIRCChecksumMismatch;
+	/***/ public String DIRCCorruptLength;
+	/***/ public String DIRCCorruptLengthFirst;
 	/***/ public String DIRCExtensionIsTooLargeAt;
 	/***/ public String DIRCExtensionNotSupportedByThisVersion;
 	/***/ public String DIRCHasTooManyEntries;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index eef822f..4fcf8e2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
  * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
- * Copyright (C) 2012-2013, Robin Rosenberg and others
+ * Copyright (C) 2012, 2020, Robin Rosenberg and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -662,4 +662,33 @@
 	 * @since 5.8
 	 */
 	public static final String CONFIG_KEY_WINDOW_MEMORY = "windowmemory";
+
+	/**
+	 * The "feature" section
+	 *
+	 * @since 5.9
+	 */
+	public static final String CONFIG_FEATURE_SECTION = "feature";
+
+	/**
+	 * The "feature.manyFiles" key
+	 *
+	 * @since 5.9
+	 */
+	public static final String CONFIG_KEY_MANYFILES = "manyFiles";
+
+	/**
+	 * The "index" section
+	 *
+	 * @since 5.9
+	 */
+	public static final String CONFIG_INDEX_SECTION = "index";
+
+	/**
+	 * The "index.version" key
+	 *
+	 * @since 5.9
+	 */
+	public static final String CONFIG_KEY_VERSION = "version";
+
 }