Merge branch 'master' into stable-7.2

* master:
  blame.cache: Define interface and POJO to cache blame results
  CgitMidxCompatibilityTest: remove unnecessary cast
  Replace usage of deprecated Config#getEnum method
  Update GetRefsBenchmark to use 3 state core.trustStat
  URIish: fix stack overflow in regex matching
  BlameGeneratorTest: Extract "file.txt" to a constant
  MultiPackIndexWriter: add missing @since for new API class
  DefaultTypedConfigGetter: Box values to avoid infinite recursion
  midx.MultiPackIndexPrettyPrinter: pretty printer to debug multi pack index
  midx.MultiPackIndexWriter: a writer for the multipack index format
  test/tests.bzl: Add docstrings to module and function
  [ssh known_hosts] Correct parsing of host key lines
  Improve configuration of trusting file attributes in FileSnapshot
  Config: add getters for primitive types without default value
  CommitConfig: fix potential NPE
  test.BUILD: add rule for "external tests"
  midx.PackIndexMerger: Helper to iterate over n-indexes
  PackIndex.MutableEntry: new methods to copy its contents
  PackDirectory: make template variables names more readable
  Log pruned packfiles as debug and not warn logs
  LooseObjects: compute loose object path before retry loop
  LooseObjects: convert while loop into for loop

Change-Id: Ia17db405d32f26f730c5206606c1ad3ff0ea28ee
diff --git a/.bazelrc b/.bazelrc
index 74601dc..70322dd 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -37,5 +37,6 @@
 test --build_tests_only
 test --test_output=errors
 test --flaky_test_attempts=3
+test --test_tag_filters=-ext
 
 import %workspace%/tools/remote-bazelrc
diff --git a/Documentation/config-options.md b/Documentation/config-options.md
index eeb78ff..807d4a8 100644
--- a/Documentation/config-options.md
+++ b/Documentation/config-options.md
@@ -55,9 +55,12 @@
 | `core.streamFileThreshold` | `50 MiB` | ⃞ | The size threshold beyond which objects must be streamed. |
 | `core.supportsAtomicFileCreation` | `true` | ⃞ | Whether the filesystem supports atomic file creation. |
 | `core.symlinks` | Auto detect if filesystem supports symlinks| ✅ | If false, symbolic links are checked out as small plain files that contain the link text. |
-| `core.trustFolderStat` | `true` | ⃞ | Whether to trust the pack folder's, packed-refs file's and loose-objects folder's file attributes (Java equivalent of stat command on *nix). When looking for pack files, if `false` JGit will always scan the `.git/objects/pack` folder and if set to `true` it assumes that pack files are unchanged if the file attributes of the pack folder are unchanged. When getting the list of packed refs, if `false` JGit will always read the packed-refs file and if set to `true` it uses the file attributes of the packed-refs file and will only read it if a file attribute has changed. When looking for loose objects, if `false` and if a loose object is not found, JGit will open and close a stream to `.git/objects` folder (which can refresh its directory listing, at least on some NFS clients) and retry looking for that loose object. Setting this option to `false` can help to workaround caching issues on NFS, but reduces performance. |
-| `core.trustPackedRefsStat` | `unset` | ⃞ | Whether to trust the file attributes (Java equivalent of stat command on *nix) of the packed-refs file. If `never` JGit will ignore the file attributes of the packed-refs file and always read it. If `always` JGit will trust the file attributes of the packed-refs file and will only read it if a file attribute has changed. `after_open` behaves the same as `always`, except that the packed-refs file is opened and closed before its file attributes are considered. An open/close of the packed-refs file is known to refresh its file attributes, at least on some NFS clients. If `unset`, JGit will use the behavior described in `trustFolderStat`. |
-| `core.trustLooseRefStat` | `always` | ⃞ | Whether to trust the file attributes (Java equivalent of stat command on *nix) of the loose ref. If `always` JGit will trust the file attributes of the loose ref and its parent directories. `after_open` behaves similar to `always`, except that all parent directories of the loose ref up to the repository root are opened and closed before its file attributes are considered. An open/close of these parent directories is known to refresh the file attributes, at least on some NFS clients. |
+| ~~`core.trustFolderStat`~~ | `true` | ⃞ | __Deprecated__, use `core.trustStat` instead. If set to `true` translated to `core.trustStat=always`, if `false` translated to `core.trustStat=never`, see below. If both `core.trustFolderStat` and `core.trustStat` are configured then `trustStat` takes precedence and `trustFolderStat` is ignored. |
+| `core.trustLooseRefStat` | `inherit` | ⃞ | Whether to trust the file attributes of loose refs and its fan-out parent directory. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `trustStat`. |
+| `core.trustPackedRefsStat` | `inherit` | ⃞ | Whether to trust the file attributes of the packed-refs file. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `core.trustStat`. |
+| `core.trustLooseObjectStat` | `inherit` | ⃞ | Whether to trust the file attributes of the loose object file and its fan-out parent directory. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `core.trustStat`. |
+| `core.trustPackStat` | `inherit` | ⃞ | Whether to trust the file attributes of the `objects/pack` directory. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `core.trustStat`. |
+| `core.trustStat` | `always` | ⃞ | Global option to configure whether to trust file attributes (Java equivalent of stat command on Unix) of files storing git objects. Can be overridden for specific files by configuring `core.trustLooseRefStat, core.trustPackedRefsStat, core.trustLooseObjectStat, core.trustPackStat`. If `never` JGit will ignore the file attributes of the file and always read it. If `always` JGit will trust the file attributes and will only read it if a file attribute has changed. `after_open` behaves the same as `always`, but file attributes are only considered *after* the file itself and any transient parent directories have been opened and closed. An open/close of the file/directory is known to refresh its file attributes, at least on some NFS clients. |
 | `core.worktree` | Root directory of the working tree if it is not the parent directory of the `.git` directory | ✅ | The path to the root of the working tree. |
 
 ## __fetch__ options
diff --git a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
index 52a881b..ea279fb 100644
--- a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
+++ b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
@@ -28,6 +28,7 @@
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
@@ -66,11 +67,11 @@ public static class BenchmarkState {
 		@Param({ "true", "false" })
 		boolean useRefTable;
 
-		@Param({ "100", "2500", "10000", "50000" })
+		@Param({ "100", "1000", "10000", "100000" })
 		int numBranches;
 
-		@Param({ "true", "false" })
-		boolean trustFolderStat;
+		@Param({ "ALWAYS", "AFTER_OPEN", "NEVER" })
+		TrustStat trustStat;
 
 		List<String> branches = new ArrayList<>(numBranches);
 
@@ -83,8 +84,8 @@ public static class BenchmarkState {
 		public void setupBenchmark() throws IOException, GitAPIException {
 			String firstBranch = "firstbranch";
 			testDir = Files.createDirectory(Paths.get("testrepos"));
-			String repoName = "branches-" + numBranches + "-trustFolderStat-"
-					+ trustFolderStat + "-" + refDatabaseType();
+			String repoName = "branches-" + numBranches + "-trustStat-"
+					+ trustStat + "-" + refDatabaseType();
 			Path workDir = testDir.resolve(repoName);
 			Path repoPath = workDir.resolve(".git");
 			Git git = Git.init().setDirectory(workDir.toFile()).call();
@@ -98,9 +99,9 @@ public void setupBenchmark() throws IOException, GitAPIException {
 						ConfigConstants.CONFIG_REF_STORAGE_REFTABLE, false,
 						false);
 			} else {
-				cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT,
-						trustFolderStat);
+				cfg.setEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
+						ConfigConstants.CONFIG_KEY_TRUST_STAT,
+						trustStat);
 			}
 			cfg.setInt(ConfigConstants.CONFIG_RECEIVE_SECTION, null,
 					"maxCommandBytes", Integer.MAX_VALUE);
@@ -112,7 +113,7 @@ public void setupBenchmark() throws IOException, GitAPIException {
 			System.out.println("Preparing test");
 			System.out.println("- repository: \t\t" + repoPath);
 			System.out.println("- refDatabase: \t\t" + refDatabaseType());
-			System.out.println("- trustFolderStat: \t" + trustFolderStat);
+			System.out.println("- trustStat: \t" + trustStat);
 			System.out.println("- branches: \t\t" + numBranches);
 
 			BatchRefUpdate u = repo.getRefDatabase().newBatchUpdate();
@@ -152,7 +153,7 @@ public void teardown() throws IOException {
 	@BenchmarkMode({ Mode.AverageTime })
 	@OutputTimeUnit(TimeUnit.MICROSECONDS)
 	@Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS)
-	@Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
 	public void testGetExactRef(Blackhole blackhole, BenchmarkState state)
 			throws IOException {
 		String branchName = state.branches
@@ -164,7 +165,7 @@ public void testGetExactRef(Blackhole blackhole, BenchmarkState state)
 	@BenchmarkMode({ Mode.AverageTime })
 	@OutputTimeUnit(TimeUnit.MICROSECONDS)
 	@Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS)
-	@Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
 	public void testGetRefsByPrefix(Blackhole blackhole, BenchmarkState state)
 			throws IOException {
 		String branchPrefix = "refs/heads/branch/" + branchIndex.nextInt(100)
diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
index f266ce1..32c12a1 100644
--- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
@@ -27,6 +27,7 @@
  org.eclipse.jgit.api.errors;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.signing.ssh;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.storage.file;version="[7.2.0,7.3.0)",
+ org.eclipse.jgit.internal.transport.sshd;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.transport.sshd.proxy;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.junit;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.junit.ssh;version="[7.2.0,7.3.0)",
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReaderTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReaderTest.java
new file mode 100644
index 0000000..d36c38f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReaderTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> 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.internal.transport.sshd;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.junit.Test;
+
+public class KnownHostEntryReaderTest {
+
+	@Test
+	public void testUnsupportedHostKeyLine() {
+		KnownHostEntry entry = KnownHostEntryReader.parseHostEntry(
+				"[localhost]:2222 ssh-unknown AAAAC3NzaC1lZDI1NTE5AAAAIPu6ntmyfSOkqLl3qPxD5XxwW7OONwwSG3KO+TGn+PFu");
+		AuthorizedKeyEntry keyEntry = entry.getKeyEntry();
+		assertNotNull(keyEntry);
+		assertEquals("ssh-unknown", keyEntry.getKeyType());
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index b6b528c..822ef55 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -9,7 +9,7 @@
 Bundle-Version: 7.2.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Export-Package: org.eclipse.jgit.internal.signing.ssh;version="7.2.0";x-friends:="org.eclipse.jgit.ssh.apache.test",
- org.eclipse.jgit.internal.transport.sshd;version="7.2.0";x-internal:=true;
+ org.eclipse.jgit.internal.transport.sshd;version="7.2.0";x-friends:="org.eclipse.jgit.ssh.apache.test";
   uses:="org.apache.sshd.client,
    org.apache.sshd.client.auth,
    org.apache.sshd.client.auth.keyboard,
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java
index 96829b7..6b2345d 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java
@@ -29,6 +29,7 @@
 import org.apache.sshd.client.config.hosts.KnownHostEntry;
 import org.apache.sshd.client.config.hosts.KnownHostHashValue;
 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -97,7 +98,7 @@ private static String clean(String line) {
 		return i < 0 ? line.trim() : line.substring(0, i).trim();
 	}
 
-	private static KnownHostEntry parseHostEntry(String line) {
+	static KnownHostEntry parseHostEntry(String line) {
 		KnownHostEntry entry = new KnownHostEntry();
 		entry.setConfigLine(line);
 		String tmp = line;
@@ -135,8 +136,8 @@ private static KnownHostEntry parseHostEntry(String line) {
 			entry.setPatterns(patterns);
 		}
 		tmp = tmp.substring(i + 1).trim();
-		AuthorizedKeyEntry key = AuthorizedKeyEntry
-				.parseAuthorizedKeyEntry(tmp);
+		AuthorizedKeyEntry key = PublicKeyEntry
+				.parsePublicKeyEntry(new AuthorizedKeyEntry(), tmp);
 		if (key == null) {
 			return null;
 		}
diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD
index 29f5b36..7755df0 100644
--- a/org.eclipse.jgit.test/BUILD
+++ b/org.eclipse.jgit.test/BUILD
@@ -53,6 +53,11 @@
     exclude = HELPERS + DATA + EXCLUDED,
 ))
 
+
+tests(tests = glob(["exttst/**/*.java"]),
+    srcprefix = "exttst/",
+    extra_tags = ["ext"])
+
 # Non abstract base classes used for tests by other test classes
 BASE = [
     PKG + "internal/storage/file/FileRepositoryBuilderTest.java",
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 5a5dd88..af5fd1c 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -48,6 +48,7 @@
  org.eclipse.jgit.internal.storage.file;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.storage.io;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.storage.memory;version="[7.2.0,7.3.0)",
+ org.eclipse.jgit.internal.storage.midx;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.storage.pack;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.storage.reftable;version="[7.2.0,7.3.0)",
  org.eclipse.jgit.internal.transport.connectivity;version="[7.2.0,7.3.0)",
diff --git a/org.eclipse.jgit.test/exttst/org/eclipse/jgit/internal/storage/midx/CgitMidxCompatibilityTest.java b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/internal/storage/midx/CgitMidxCompatibilityTest.java
new file mode 100644
index 0000000..88f0806
--- /dev/null
+++ b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/internal/storage/midx/CgitMidxCompatibilityTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.Pack;
+import org.eclipse.jgit.internal.storage.file.PackFile;
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
+import org.eclipse.jgit.util.NB;
+import org.junit.Test;
+
+public class CgitMidxCompatibilityTest extends SampleDataRepositoryTestCase {
+
+	@Test
+	public void jgitMidx_verifyByCgit()
+			throws IOException, InterruptedException {
+		byte[] jgitMidxBytes = generateJGitMidx();
+		writeMidx(jgitMidxBytes);
+		assertEquals("cgit exit code", 0, run_cgit_multipackindex_verify());
+	}
+
+	@Test
+	public void compareBasicChunkSizes()
+			throws IOException, InterruptedException {
+		// We cannot compare byte-by-byte because there are optional chunks and
+		// it is not guaranteed what cgit and jgit will generate
+		byte[] jgitMidxBytes = generateJGitMidx();
+		assertEquals("cgit exit code", 0, run_cgit_multipackindex_write());
+		byte[] cgitMidxBytes = readCgitMidx();
+
+		RawMultiPackIndex jgitMidx = new RawMultiPackIndex(jgitMidxBytes);
+		RawMultiPackIndex cgitMidx = new RawMultiPackIndex(cgitMidxBytes);
+
+		// This is a fixed sized chunk
+		assertEquals(256 * 4, cgitMidx.getChunkSize(MIDX_CHUNKID_OIDFANOUT));
+		assertArrayEquals(cgitMidx.getRawChunk(MIDX_CHUNKID_OIDFANOUT),
+				jgitMidx.getRawChunk(MIDX_CHUNKID_OIDFANOUT));
+
+		assertArrayEquals(cgitMidx.getRawChunk(MIDX_CHUNKID_OIDLOOKUP),
+				jgitMidx.getRawChunk(MIDX_CHUNKID_OIDLOOKUP));
+
+		// The spec has changed from padding packnames to a multile of four, to
+		// move the packname chunk to the end of the file.
+		// git 2.48 pads the packs names to a multiple of 4
+		// jgit puts the chunk at the end
+		byte[] cgitPacknames = trimPadding(
+				cgitMidx.getRawChunk(MIDX_CHUNKID_PACKNAMES));
+		assertArrayEquals(cgitPacknames,
+				jgitMidx.getRawChunk(MIDX_CHUNKID_PACKNAMES));
+
+		assertArrayEquals(cgitMidx.getRawChunk(MIDX_CHUNKID_OBJECTOFFSETS),
+				jgitMidx.getRawChunk(MIDX_CHUNKID_OBJECTOFFSETS));
+
+	}
+
+	private byte[] generateJGitMidx() throws IOException {
+		Map<String, PackIndex> indexes = new HashMap<>();
+		for (Pack pack : db.getObjectDatabase().getPacks()) {
+			PackFile packFile = pack.getPackFile().create(PackExt.INDEX);
+			indexes.put(packFile.getName(), pack.getIndex());
+		}
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, indexes);
+		return out.toByteArray();
+	}
+
+	private int run_cgit_multipackindex_write()
+			throws IOException, InterruptedException {
+		String[] command = new String[] { "git", "multi-pack-index", "write" };
+		Process proc = Runtime.getRuntime().exec(command, new String[0],
+				db.getDirectory());
+		return proc.waitFor();
+	}
+
+	private int run_cgit_multipackindex_verify()
+			throws IOException, InterruptedException {
+		String[] command = new String[] { "git", "multi-pack-index", "verify" };
+		Process proc = Runtime.getRuntime().exec(command, new String[0],
+				db.getDirectory());
+		return proc.waitFor();
+	}
+
+	private byte[] readCgitMidx() throws IOException {
+		File midx = getMIdxStandardLocation();
+		assertTrue("cgit multi-pack-index exists", midx.exists());
+		return Files.readAllBytes(midx.toPath());
+	}
+
+	private void writeMidx(byte[] midx) throws IOException {
+		File midxFile = getMIdxStandardLocation();
+		Files.write(midxFile.toPath(), midx);
+	}
+
+	private File getMIdxStandardLocation() {
+		return new File(db.getObjectDatabase().getPackDirectory(),
+				"multi-pack-index");
+	}
+
+	private byte[] trimPadding(byte[] data) {
+		// Chunk MUST have one \0, we want to remove any extra \0
+		int newEnd = data.length - 1;
+		while (newEnd - 1 >= 0 && data[newEnd - 1] == 0) {
+			newEnd--;
+		}
+
+		if (newEnd == data.length - 1) {
+			return data;
+		}
+		return Arrays.copyOfRange(data, 0, newEnd + 1);
+	}
+
+	private static class RawMultiPackIndex {
+		private final List<ChunkSegment> chunks;
+
+		private final byte[] midx;
+
+		private RawMultiPackIndex(byte[] midx) {
+			this.chunks = readChunks(midx);
+			this.midx = midx;
+		}
+
+		long getChunkSize(int chunkId) {
+			int chunkPos = findChunkPosition(chunks, chunkId);
+			return chunks.get(chunkPos + 1).offset
+					- chunks.get(chunkPos).offset;
+		}
+
+		long getOffset(int chunkId) {
+			return chunks.get(findChunkPosition(chunks, chunkId)).offset;
+		}
+
+		private long getNextOffset(int chunkId) {
+			return chunks.get(findChunkPosition(chunks, chunkId) + 1).offset;
+		}
+
+		byte[] getRawChunk(int chunkId) {
+			int start = (int) getOffset(chunkId);
+			int end = (int) getNextOffset(chunkId);
+			return Arrays.copyOfRange(midx, start, end);
+		}
+
+		private static int findChunkPosition(List<ChunkSegment> chunks,
+				int id) {
+			int chunkPos = -1;
+			for (int i = 0; i < chunks.size(); i++) {
+				if (chunks.get(i).id() == id) {
+					chunkPos = i;
+					break;
+				}
+			}
+			if (chunkPos == -1) {
+				throw new IllegalStateException("Chunk doesn't exist");
+			}
+			return chunkPos;
+		}
+
+		private List<ChunkSegment> readChunks(byte[] midx) {
+			// Read the number of "chunkOffsets" (1 byte)
+			int chunkCount = midx[6];
+			byte[] lookupBuffer = new byte[CHUNK_LOOKUP_WIDTH
+					* (chunkCount + 1)];
+			System.arraycopy(midx, 12, lookupBuffer, 0, lookupBuffer.length);
+
+			List<ChunkSegment> chunks = new ArrayList<>(chunkCount + 1);
+			for (int i = 0; i <= chunkCount; i++) {
+				// chunks[chunkCount] is just a marker, in order to record the
+				// length of the last chunk.
+				int id = NB.decodeInt32(lookupBuffer, i * 12);
+				long offset = NB.decodeInt64(lookupBuffer, i * 12 + 4);
+				chunks.add(new ChunkSegment(id, offset));
+			}
+			return chunks;
+		}
+	}
+
+	private record ChunkSegment(int id, long offset) {
+	}
+}
diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl
index 170bf0c..41f76d0 100644
--- a/org.eclipse.jgit.test/tests.bzl
+++ b/org.eclipse.jgit.test/tests.bzl
@@ -1,11 +1,29 @@
+'''
+Expose each test as a bazel target
+'''
 load(
     "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
     "junit_tests",
 )
 
-def tests(tests):
+def tests(tests, srcprefix="tst/", extra_tags=[]):
+    '''
+    Create a target each of the tests
+
+    Each target is the full push (removing srcprefix) replacing directory
+    separators with underscores.
+
+    e.g. a test under tst/a/b/c/A.test will become the target
+    //org.eclipse.jgit.tests:a_b_c_A
+
+    Args:
+      tests: a glob of tests files
+      srcprefix: prefix between org.eclipse.jgit.tests and the package
+        start
+      extra_tags: additional tags to add to the generated targets
+    '''
     for src in tests:
-        name = src[len("tst/"):len(src) - len(".java")].replace("/", "_")
+        name = src[len(srcprefix):len(src) - len(".java")].replace("/", "_")
         labels = []
         timeout = "moderate"
         if name.startswith("org_eclipse_jgit_"):
@@ -20,6 +38,8 @@
         if "lib" not in labels:
             labels.append("lib")
 
+        labels.extend(extra_tags)
+
         # TODO(http://eclip.se/534285): Make this test pass reliably
         # and remove the flaky attribute.
         flaky = src.endswith("CrissCrossMergeTest.java")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
index f5fd73b..661878f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
@@ -797,7 +797,7 @@ public void testCloneWithAutoSetupRebase() throws Exception {
 		assertNull(git2.getRepository().getConfig().getEnum(
 				BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, "test",
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 
 		StoredConfig userConfig = SystemReader.getInstance()
 				.getUserConfig();
@@ -813,7 +813,6 @@ public void testCloneWithAutoSetupRebase() throws Exception {
 		addRepoToClose(git2.getRepository());
 		assertEquals(BranchRebaseMode.REBASE,
 				git2.getRepository().getConfig().getEnum(
-						BranchRebaseMode.values(),
 						ConfigConstants.CONFIG_BRANCH_SECTION, "test",
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
@@ -830,7 +829,6 @@ public void testCloneWithAutoSetupRebase() throws Exception {
 		addRepoToClose(git2.getRepository());
 		assertEquals(BranchRebaseMode.REBASE,
 				git2.getRepository().getConfig().getEnum(
-						BranchRebaseMode.values(),
 						ConfigConstants.CONFIG_BRANCH_SECTION, "test",
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
index 534ebd9..add5886 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
@@ -118,23 +118,21 @@ public void renameBranchSingleConfigValue() throws Exception {
 		String branch = "b1";
 
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-						ConfigConstants.CONFIG_KEY_REBASE,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION,
+						Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, branch,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 
 		assertNotNull(git.branchRename().setNewName(branch).call());
 
 		config = git.getRepository().getConfig();
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, branch,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 	}
@@ -170,13 +168,12 @@ public void renameBranchMultipleConfigValues() throws Exception {
 		String branch = "b1";
 
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-						ConfigConstants.CONFIG_KEY_REBASE,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION,
+						Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, branch,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 		assertTrue(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
 				Constants.MASTER, ConfigConstants.CONFIG_KEY_MERGE, true));
 		assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
@@ -187,10 +184,9 @@ public void renameBranchMultipleConfigValues() throws Exception {
 		config = git.getRepository().getConfig();
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, branch,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 		assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
index f47f447..c2c06b2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
@@ -23,20 +23,22 @@
 
 /** Unit tests of {@link BlameGenerator}. */
 public class BlameGeneratorTest extends RepositoryTestCase {
+	private static final String FILE = "file.txt";
+
 	@Test
 	public void testBoundLineDelete() throws Exception {
 		try (Git git = new Git(db)) {
 			String[] content1 = new String[] { "first", "second" };
-			writeTrashFile("file.txt", join(content1));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content1));
+			git.add().addFilepattern(FILE).call();
 			RevCommit c1 = git.commit().setMessage("create file").call();
 
 			String[] content2 = new String[] { "third", "first", "second" };
-			writeTrashFile("file.txt", join(content2));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content2));
+			git.add().addFilepattern(FILE).call();
 			RevCommit c2 = git.commit().setMessage("create file").call();
 
-			try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) {
+			try (BlameGenerator generator = new BlameGenerator(db, FILE)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				assertEquals(3, generator.getResultContents().size());
 
@@ -47,7 +49,7 @@ public void testBoundLineDelete() throws Exception {
 				assertEquals(1, generator.getResultEnd());
 				assertEquals(0, generator.getSourceStart());
 				assertEquals(1, generator.getSourceEnd());
-				assertEquals("file.txt", generator.getSourcePath());
+				assertEquals(FILE, generator.getSourcePath());
 
 				assertTrue(generator.next());
 				assertEquals(c1, generator.getSourceCommit());
@@ -56,7 +58,7 @@ public void testBoundLineDelete() throws Exception {
 				assertEquals(3, generator.getResultEnd());
 				assertEquals(0, generator.getSourceStart());
 				assertEquals(2, generator.getSourceEnd());
-				assertEquals("file.txt", generator.getSourcePath());
+				assertEquals(FILE, generator.getSourcePath());
 
 				assertFalse(generator.next());
 			}
@@ -87,7 +89,8 @@ public void testRenamedBoundLineDelete() throws Exception {
 			git.add().addFilepattern(FILENAME_2).call();
 			RevCommit c2 = git.commit().setMessage("change file2").call();
 
-			try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) {
+			try (BlameGenerator generator = new BlameGenerator(db,
+					FILENAME_2)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				assertEquals(3, generator.getResultContents().size());
 
@@ -113,7 +116,8 @@ public void testRenamedBoundLineDelete() throws Exception {
 			}
 
 			// and test again with other BlameGenerator API:
-			try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) {
+			try (BlameGenerator generator = new BlameGenerator(db,
+					FILENAME_2)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				BlameResult result = generator.computeBlameResult();
 
@@ -136,21 +140,21 @@ public void testLinesAllDeletedShortenedWalk() throws Exception {
 		try (Git git = new Git(db)) {
 			String[] content1 = new String[] { "first", "second", "third" };
 
-			writeTrashFile("file.txt", join(content1));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content1));
+			git.add().addFilepattern(FILE).call();
 			git.commit().setMessage("create file").call();
 
 			String[] content2 = new String[] { "" };
 
-			writeTrashFile("file.txt", join(content2));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content2));
+			git.add().addFilepattern(FILE).call();
 			git.commit().setMessage("create file").call();
 
-			writeTrashFile("file.txt", join(content1));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content1));
+			git.add().addFilepattern(FILE).call();
 			RevCommit c3 = git.commit().setMessage("create file").call();
 
-			try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) {
+			try (BlameGenerator generator = new BlameGenerator(db, FILE)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				assertEquals(3, generator.getResultContents().size());
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
index 7d298ee..33cbc86 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
@@ -50,6 +50,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -210,17 +211,17 @@ public void testOpenLooseObjectSuppressStaleFileHandleException()
 				.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b");
 		WindowCursor curs = new WindowCursor(db.getObjectDatabase());
 
-		LooseObjects mock = mock(LooseObjects.class);
+		Config config = new Config();
+		config.setString("core", null, "trustLooseObjectStat", "ALWAYS");
+		LooseObjects spy = Mockito.spy(new LooseObjects(config, trash));
 		UnpackedObjectCache unpackedObjectCacheMock = mock(
 				UnpackedObjectCache.class);
 
-		Mockito.when(mock.getObjectLoader(any(), any(), any()))
-				.thenThrow(new IOException("Stale File Handle"));
-		Mockito.when(mock.open(curs, id)).thenCallRealMethod();
-		Mockito.when(mock.unpackedObjectCache())
-				.thenReturn(unpackedObjectCacheMock);
+		doThrow(new IOException("Stale File Handle")).when(spy)
+				.getObjectLoader(any(), any(), any());
+		doReturn(unpackedObjectCacheMock).when(spy).unpackedObjectCache();
 
-		assertNull(mock.open(curs, id));
+		assertNull(spy.open(curs, id));
 		verify(unpackedObjectCacheMock).remove(id);
 	}
 
@@ -231,7 +232,7 @@ public void testOpenLooseObjectPropagatesIOExceptions() throws Exception {
 		WindowCursor curs = new WindowCursor(db.getObjectDatabase());
 
 		Config config = new Config();
-		config.setString("core", null, "trustFolderStat", "false");
+		config.setString("core", null, "trustLooseObjectStat", "NEVER");
 		LooseObjects spy = spy(new LooseObjects(config,
 				db.getObjectDatabase().getDirectory()));
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
index 24bdc4a..1f934ac 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
@@ -13,6 +13,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import java.io.File;
@@ -25,6 +26,7 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
@@ -99,6 +101,39 @@ public void testIteratorMethodsContract() {
 		}
 	}
 
+	@Test
+	public void testIteratorMutableEntryCompareTo() {
+		Iterator<PackIndex.MutableEntry> iterA = smallIdx.iterator();
+		Iterator<PackIndex.MutableEntry> iterB = smallIdx.iterator();
+
+		MutableEntry aEntry = iterA.next();
+		iterB.next();
+		MutableEntry bEntry = iterB.next();
+		// b is one ahead
+		assertTrue(aEntry.compareBySha1To(bEntry) < 0);
+		assertTrue(bEntry.compareBySha1To(aEntry) > 0);
+
+		// advance a, now should be equal
+		assertEquals(0, iterA.next().compareBySha1To(bEntry));
+	}
+
+	@Test
+	public void testIteratorMutableEntryCopyTo() {
+		Iterator<PackIndex.MutableEntry> it = smallIdx.iterator();
+
+		MutableObjectId firstOidCopy = new MutableObjectId();
+		MutableEntry next = it.next();
+		next.copyOidTo(firstOidCopy);
+		ObjectId firstImmutable = next.toObjectId();
+
+		MutableEntry second = it.next();
+
+		// The copy has the right value after "next"
+		assertTrue(firstImmutable.equals(firstOidCopy));
+		assertFalse("iterator has moved",
+				second.toObjectId().equals(firstImmutable));
+	}
+
 	/**
 	 * Test results of iterator comparing to content of well-known (prepared)
 	 * small index.
@@ -106,22 +141,22 @@ public void testIteratorMethodsContract() {
 	@Test
 	public void testIteratorReturnedValues1() {
 		Iterator<PackIndex.MutableEntry> iter = smallIdx.iterator();
-		assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904", iter.next()
-				.name());
-		assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab", iter.next()
-				.name());
-		assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259", iter.next()
-				.name());
-		assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3", iter.next()
-				.name());
-		assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7", iter.next()
-				.name());
-		assertEquals("902d5476fa249b7abc9d84c611577a81381f0327", iter.next()
-				.name());
-		assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035", iter.next()
-				.name());
-		assertEquals("c59759f143fb1fe21c197981df75a7ee00290799", iter.next()
-				.name());
+		assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+				iter.next().name());
+		assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab",
+				iter.next().name());
+		assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259",
+				iter.next().name());
+		assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3",
+				iter.next().name());
+		assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7",
+				iter.next().name());
+		assertEquals("902d5476fa249b7abc9d84c611577a81381f0327",
+				iter.next().name());
+		assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035",
+				iter.next().name());
+		assertEquals("c59759f143fb1fe21c197981df75a7ee00290799",
+				iter.next().name());
 		assertFalse(iter.hasNext());
 	}
 
@@ -198,16 +233,16 @@ public void testCompareEntriesOffsetsWithGetOffsets() {
 	@Test
 	public void testIteratorReturnedValues2() {
 		Iterator<PackIndex.MutableEntry> iter = denseIdx.iterator();
-		while (!iter.next().name().equals(
-				"0a3d7772488b6b106fb62813c4d6d627918d9181")) {
+		while (!iter.next().name()
+				.equals("0a3d7772488b6b106fb62813c4d6d627918d9181")) {
 			// just iterating
 		}
-		assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3", iter.next()
-				.name()); // same level-1
-		assertEquals("10da5895682013006950e7da534b705252b03be6", iter.next()
-				.name()); // same level-1
-		assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8", iter.next()
-				.name());
+		assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3",
+				iter.next().name()); // same level-1
+		assertEquals("10da5895682013006950e7da534b705252b03be6",
+				iter.next().name()); // same level-1
+		assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8",
+				iter.next().name());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java
new file mode 100644
index 0000000..82f3eb1
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.util.NB;
+import org.junit.Test;
+
+public class MultiPackIndexWriterTest {
+
+	@Test
+	public void write_allSmallOffsets() throws IOException {
+		PackIndex index1 = indexOf(
+				object("0000000000000000000000000000000000000001", 500),
+				object("0000000000000000000000000000000000000003", 1500),
+				object("0000000000000000000000000000000000000005", 3000));
+		PackIndex index2 = indexOf(
+				object("0000000000000000000000000000000000000002", 500),
+				object("0000000000000000000000000000000000000004", 1500),
+				object("0000000000000000000000000000000000000006", 3000));
+
+		Map<String, PackIndex> data = Map.of("packname1", index1, "packname2",
+				index2);
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, data);
+		// header (12 bytes)
+		// + chunkHeader (6 * 12 bytes)
+		// + fanout table (256 * 4 bytes)
+		// + OIDs (6 * 20 bytes)
+		// + (pack, offset) pairs (6 * 8)
+		// + RIDX (6 * 4 bytes)
+		// + packfile names (2 * 10)
+		// + checksum (20)
+		assertEquals(1340, out.size());
+		List<Integer> chunkIds = readChunkIds(out);
+		assertEquals(5, chunkIds.size());
+		assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+		assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+		assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+		assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+		assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+	}
+
+	@Test
+	public void write_smallOffset_limit() throws IOException {
+		PackIndex index1 = indexOf(
+				object("0000000000000000000000000000000000000001", 500),
+				object("0000000000000000000000000000000000000003", 1500),
+				object("0000000000000000000000000000000000000005", (1L << 32) -1));
+		PackIndex index2 = indexOf(
+				object("0000000000000000000000000000000000000002", 500),
+				object("0000000000000000000000000000000000000004", 1500),
+				object("0000000000000000000000000000000000000006", 3000));
+		Map<String, PackIndex> data =
+				Map.of("packname1", index1, "packname2", index2);
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, data);
+		// header (12 bytes)
+		// + chunkHeader (6 * 12 bytes)
+		// + fanout table (256 * 4 bytes)
+		// + OIDs (6 * 20 bytes)
+		// + (pack, offset) pairs (6 * 8)
+		// + RIDX (6 * 4 bytes)
+		// + packfile names (2 * 10)
+		// + checksum (20)
+		assertEquals(1340, out.size());
+		List<Integer> chunkIds = readChunkIds(out);
+		assertEquals(5, chunkIds.size());
+		assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+		assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+		assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+		assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+		assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+	}
+
+	@Test
+	public void write_largeOffset() throws IOException {
+		PackIndex index1 = indexOf(
+				object("0000000000000000000000000000000000000001", 500),
+				object("0000000000000000000000000000000000000003", 1500),
+				object("0000000000000000000000000000000000000005", 1L << 32));
+		PackIndex index2 = indexOf(
+				object("0000000000000000000000000000000000000002", 500),
+				object("0000000000000000000000000000000000000004", 1500),
+				object("0000000000000000000000000000000000000006", 3000));
+		Map<String, PackIndex> data =
+				Map.of("packname1", index1, "packname2", index2);
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, data);
+		// header (12 bytes)
+		// + chunkHeader (7 * 12 bytes)
+		// + fanout table (256 * 4 bytes)
+		// + OIDs (6 * 20 bytes)
+		// + (pack, offset) pairs (6 * 8)
+		// + (large-offset) (1 * 8)
+		// + RIDX (6 * 4 bytes)
+		// + packfile names (2 * 10)
+		// + checksum (20)
+		assertEquals(1360, out.size());
+		List<Integer> chunkIds = readChunkIds(out);
+		assertEquals(6, chunkIds.size());
+		assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+		assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+		assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+		assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_LARGEOFFSETS));
+		assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+		assertEquals(5, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+	}
+
+	private List<Integer> readChunkIds(ByteArrayOutputStream out) {
+		List<Integer> chunkIds = new ArrayList<>();
+		byte[] raw = out.toByteArray();
+		int numChunks = raw[6];
+		int position = 12;
+		for (int i = 0; i < numChunks; i++) {
+			chunkIds.add(NB.decodeInt32(raw, position));
+			position += CHUNK_LOOKUP_WIDTH;
+		}
+		return chunkIds;
+	}
+
+	private static PackIndex indexOf(IndexObject... objs) {
+		return FakeIndexFactory.indexOf(Arrays.asList(objs));
+	}
+
+	private static IndexObject object(String name, long offset) {
+		return new IndexObject(name, offset);
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java
new file mode 100644
index 0000000..1d8bde0
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject;
+import org.junit.Test;
+
+public class PackIndexMergerTest {
+
+	@Test
+	public void rawIterator_noDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndex idxTwo = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 501),
+				oidOffset("0000000000000000000000000000000000000003", 13),
+				oidOffset("0000000000000000000000000000000000000015", 1501));
+		PackIndex idxThree = indexOf(
+				oidOffset("0000000000000000000000000000000000000004", 502),
+				oidOffset("0000000000000000000000000000000000000007", 14),
+				oidOffset("0000000000000000000000000000000000000012", 1502));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+		assertEquals(9, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501);
+		assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13);
+		assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+				1502);
+		assertNextEntry(it, "0000000000000000000000000000000000000015", 1,
+				1501);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void rawIterator_allDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne));
+		assertEquals(3, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 1, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 2, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 1, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 2, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 1,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 2,
+				1500);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_noDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndex idxTwo = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 501),
+				oidOffset("0000000000000000000000000000000000000003", 13),
+				oidOffset("0000000000000000000000000000000000000015", 1501));
+		PackIndex idxThree = indexOf(
+				oidOffset("0000000000000000000000000000000000000004", 502),
+				oidOffset("0000000000000000000000000000000000000007", 14),
+				oidOffset("0000000000000000000000000000000000000012", 1502));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+		assertEquals(9, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501);
+		assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13);
+		assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+				1502);
+		assertNextEntry(it, "0000000000000000000000000000000000000015", 1,
+				1501);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_allDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne));
+		assertEquals(3, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_differentIndexSizes() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndex idxTwo = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 500),
+				oidOffset("0000000000000000000000000000000000000003", 12));
+		PackIndex idxThree = indexOf(
+				oidOffset("0000000000000000000000000000000000000004", 500),
+				oidOffset("0000000000000000000000000000000000000007", 12),
+				oidOffset("0000000000000000000000000000000000000012", 1500));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+		assertEquals(6, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+				1500);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void merger_noIndexes() {
+		PackIndexMerger merger = new PackIndexMerger(Map.of());
+		assertEquals(0, merger.getUniqueObjectCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		assertTrue(merger.getPackNames().isEmpty());
+		assertEquals(0, merger.getPackCount());
+		assertFalse(merger.bySha1Iterator().hasNext());
+	}
+
+	@Test
+	public void merger_emptyIndexes() {
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", indexOf(), "p2", indexOf()));
+		assertEquals(0, merger.getUniqueObjectCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		assertEquals(2, merger.getPackNames().size());
+		assertEquals(2, merger.getPackCount());
+		assertFalse(merger.bySha1Iterator().hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_largeOffsets_needsChunk() {
+		PackIndex idx1 = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 1L << 32),
+				oidOffset("0000000000000000000000000000000000000004", 12));
+		PackIndex idx2 = indexOf(oidOffset(
+				"0000000000000000000000000000000000000003", (1L << 31) + 10));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idx1, "p2", idx2));
+		assertTrue(merger.needsLargeOffsetsChunk());
+		assertEquals(2, merger.getOffsetsOver31BitsCount());
+		assertEquals(3, merger.getUniqueObjectCount());
+	}
+
+	@Test
+	public void bySha1Iterator_largeOffsets_noChunk() {
+		// If no value is over 2^32-1, then we don't need large offset
+		PackIndex idx1 = indexOf(
+				oidOffset("0000000000000000000000000000000000000002",
+						(1L << 31) + 15),
+				oidOffset("0000000000000000000000000000000000000004", 12));
+		PackIndex idx2 = indexOf(oidOffset(
+				"0000000000000000000000000000000000000003", (1L << 31) + 10));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idx1, "p2", idx2));
+		assertFalse(merger.needsLargeOffsetsChunk());
+		assertEquals(2, merger.getOffsetsOver31BitsCount());
+		assertEquals(3, merger.getUniqueObjectCount());
+	}
+
+	private static void assertNextEntry(
+			Iterator<PackIndexMerger.MidxMutableEntry> it, String oid,
+			int packId, long offset) {
+		assertTrue(it.hasNext());
+		PackIndexMerger.MidxMutableEntry e = it.next();
+		assertEquals(oid, e.getObjectId().name());
+		assertEquals(packId, e.getPackId());
+		assertEquals(offset, e.getOffset());
+	}
+
+	private static IndexObject oidOffset(String oid, long offset) {
+		return new IndexObject(oid, offset);
+	}
+
+	private static PackIndex indexOf(IndexObject... objs) {
+		return FakeIndexFactory.indexOf(Arrays.asList(objs));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java
new file mode 100644
index 0000000..917288a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.junit.Test;
+
+public class PackIndexPeekIteratorTest {
+    @Test
+    public void next() {
+        PackIndex index1 = indexOf(
+                object("0000000000000000000000000000000000000001", 500),
+                object("0000000000000000000000000000000000000003", 1500),
+                object("0000000000000000000000000000000000000005", 3000));
+        PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+        assertEquals("0000000000000000000000000000000000000001", it.next().name());
+        assertEquals("0000000000000000000000000000000000000003", it.next().name());
+        assertEquals("0000000000000000000000000000000000000005", it.next().name());
+        assertNull(it.next());
+    }
+
+    @Test
+    public void peek_doesNotAdvance() {
+        PackIndex index1 = indexOf(
+                object("0000000000000000000000000000000000000001", 500),
+                object("0000000000000000000000000000000000000003", 1500),
+                object("0000000000000000000000000000000000000005", 3000));
+        PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+        it.next();
+        assertEquals("0000000000000000000000000000000000000001", it.peek().name());
+        assertEquals("0000000000000000000000000000000000000001", it.peek().name());
+        it.next();
+        assertEquals("0000000000000000000000000000000000000003", it.peek().name());
+        assertEquals("0000000000000000000000000000000000000003", it.peek().name());
+        it.next();
+        assertEquals("0000000000000000000000000000000000000005", it.peek().name());
+        assertEquals("0000000000000000000000000000000000000005", it.peek().name());
+        it.next();
+        assertNull(it.peek());
+        assertNull(it.peek());
+    }
+
+    @Test
+    public void empty() {
+        PackIndex index1 = indexOf();
+        PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+        assertNull(it.next());
+        assertNull(it.peek());
+    }
+
+    private static PackIndex indexOf(FakeIndexFactory.IndexObject... objs) {
+        return FakeIndexFactory.indexOf(Arrays.asList(objs));
+    }
+
+    private static FakeIndexFactory.IndexObject object(String name, long offset) {
+        return new FakeIndexFactory.IndexObject(name, offset);
+    }
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
index 31940a1..06fee8e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
@@ -1636,6 +1636,47 @@ public void testCoreCommitGraphConfig() {
 		assertFalse(config.get(CoreConfig.KEY).enableCommitGraph());
 	}
 
+	@Test
+	public void testGetNoDefaultBoolean() {
+		Config config = new Config();
+		assertNull(config.getBoolean("foo", "bar"));
+		assertNull(config.getBoolean("foo", "bar", "baz"));
+	}
+
+	@Test
+	public void testGetNoDefaultEnum() {
+		Config config = new Config();
+		assertNull(config.getEnum(new TestEnum[] { TestEnum.ONE_TWO }, "foo",
+				"bar", "baz"));
+	}
+
+	@Test
+	public void testGetNoDefaultInt() {
+		Config config = new Config();
+		assertNull(config.getInt("foo", "bar"));
+		assertNull(config.getInt("foo", "bar", "baz"));
+	}
+	@Test
+	public void testGetNoDefaultIntInRange() {
+		Config config = new Config();
+		assertNull(config.getIntInRange("foo", "bar", 1, 5));
+		assertNull(config.getIntInRange("foo", "bar", "baz", 1, 5));
+	}
+
+	@Test
+	public void testGetNoDefaultLong() {
+		Config config = new Config();
+		assertNull(config.getLong("foo", "bar"));
+		assertNull(config.getLong("foo", "bar", "baz"));
+	}
+
+	@Test
+	public void testGetNoDefaultTimeUnit() {
+		Config config = new Config();
+		assertNull(config.getTimeUnit("foo", "bar", "baz",
+				TimeUnit.SECONDS));
+	}
+
 	private static void assertValueRoundTrip(String value)
 			throws ConfigInvalidException {
 		assertValueRoundTrip(value, value);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
index d403624..6792002 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
@@ -82,6 +82,43 @@ public void testWindowsFile2() throws Exception {
 	}
 
 	@Test
+	public void testBrokenFilePath() throws Exception {
+		String str = "D:\\\\my\\\\x";
+		URIish u = new URIish(str);
+		assertNull(u.getScheme());
+		assertFalse(u.isRemote());
+		assertEquals(str, u.getPath());
+		assertEquals(u, new URIish(str));
+	}
+
+	@Test
+	public void testStackOverflow() throws Exception {
+		StringBuilder b = new StringBuilder("D:\\");
+		for (int i = 0; i < 4000; i++) {
+			b.append("x\\");
+		}
+		String str = b.toString();
+		URIish u = new URIish(str);
+		assertNull(u.getScheme());
+		assertFalse(u.isRemote());
+		assertEquals(str, u.getPath());
+	}
+
+	@Test
+	public void testStackOverflow2() throws Exception {
+		StringBuilder b = new StringBuilder("D:\\");
+		for (int i = 0; i < 4000; i++) {
+			b.append("x\\");
+		}
+		b.append('y');
+		String str = b.toString();
+		URIish u = new URIish(str);
+		assertNull(u.getScheme());
+		assertFalse(u.isRemote());
+		assertEquals(str, u.getPath());
+	}
+
+	@Test
 	public void testRelativePath() throws Exception {
 		final String str = "../../foo/bar";
 		URIish u = new URIish(str);
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 61d6658..877a488 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -14,4 +14,50 @@
             </message_arguments>
         </filter>
     </resource>
+    <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter">
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getBoolean(Config, String, String, String, Boolean)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getInt(Config, String, String, String, Integer)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getIntInRange(Config, String, String, String, Integer, Integer, Integer)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getIntInRange(Config, String, String, String, int, int, Integer)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getLong(Config, String, String, String, Long)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getTimeUnit(Config, String, String, String, Long, TimeUnit)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/revwalk/RevWalk.java" type="org.eclipse.jgit.revwalk.RevWalk">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="isMergedIntoAnyCommit(RevCommit, Collection&lt;RevCommit&gt;)"/>
+            </message_arguments>
+        </filter>
+    </resource>
 </component>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 659ad85..f5cc16c 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -105,6 +105,7 @@
    org.eclipse.jgit.pgm",
  org.eclipse.jgit.internal.storage.memory;version="7.2.0";
   x-friends:="org.eclipse.jgit.test",
+ org.eclipse.jgit.internal.storage.midx;version="7.2.0",
  org.eclipse.jgit.internal.storage.pack;version="7.2.0";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
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 acfe812..392f4e9 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -267,6 +267,7 @@
 deletingNotSupported=Deleting {0} not supported.
 depthMustBeAt1=Depth must be >= 1
 depthWithUnshallow=Depth and unshallow can\'t be used together
+deprecatedTrustFolderStat=Option core.trustFolderStat is deprecated, replace it by core.trustStat.
 destinationIsNotAWildcard=Destination is not a wildcard.
 detachedHeadDetected=HEAD is detached
 diffToolNotGivenError=No diff tool provided and no defaults configured.
@@ -464,6 +465,7 @@
 invalidTimeUnitValue2=Invalid time unit value: {0}.{1}={2}
 invalidTimeUnitValue3=Invalid time unit value: {0}.{1}.{2}={3}
 invalidTreeZeroLengthName=Cannot append a tree entry with zero-length name
+invalidTrustStat=core.trustStat must not be set to TrustStat.INHERIT, falling back to TrustStat.ALWAYS.
 invalidURL=Invalid URL {0}
 invalidWildcards=Invalid wildcards {0}
 invalidRefSpec=Invalid refspec {0}
@@ -527,6 +529,8 @@
 month=month
 months=months
 monthsAgo={0} months ago
+multiPackIndexUnexpectedSize=MultiPack index: expected %d bytes but out has %d bytes
+multiPackIndexWritingCancelled=Multipack index writing was canceled
 multipleMergeBasesFor=Multiple merge bases for:\n  {0}\n  {1} found:\n  {2}\n  {3}
 nameMustNotBeNullOrEmpty=Ref name must not be null or empty.
 need2Arguments=Need 2 arguments
@@ -615,6 +619,7 @@
 personIdentEmailNonNull=E-mail address of PersonIdent must not be null.
 personIdentNameNonNull=Name of PersonIdent must not be null.
 postCommitHookFailed=Execution of post-commit hook failed: {0}.
+precedenceTrustConfig=Both core.trustFolderStat and core.trustStat are set, ignoring trustFolderStat since trustStat takes precedence. Remove core.trustFolderStat from your configuration.
 prefixRemote=remote:
 problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0}
 progressMonUploading=Uploading {0}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
index 0713c38..f24127b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
@@ -124,7 +124,7 @@ private FetchRecurseSubmodulesMode getRecurseMode(String path) {
 		FetchRecurseSubmodulesMode mode = repo.getConfig().getEnum(
 				FetchRecurseSubmodulesMode.values(),
 				ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
-				ConfigConstants.CONFIG_KEY_FETCH_RECURSE_SUBMODULES, null);
+				ConfigConstants.CONFIG_KEY_FETCH_RECURSE_SUBMODULES);
 		if (mode != null) {
 			return mode;
 		}
@@ -132,7 +132,7 @@ private FetchRecurseSubmodulesMode getRecurseMode(String path) {
 		// Fall back to fetch.recurseSubmodules, if set
 		mode = repo.getConfig().getEnum(FetchRecurseSubmodulesMode.values(),
 				ConfigConstants.CONFIG_FETCH_SECTION, null,
-				ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES, null);
+				ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES);
 		if (mode != null) {
 			return mode;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
index 83ae0fc..4b2cee4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -533,9 +533,9 @@ public static BranchRebaseMode getRebaseMode(String branchName,
 			Config config) {
 		BranchRebaseMode mode = config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION,
-				branchName, ConfigConstants.CONFIG_KEY_REBASE, null);
+				branchName, ConfigConstants.CONFIG_KEY_REBASE);
 		if (mode == null) {
-			mode = config.getEnum(BranchRebaseMode.values(),
+			mode = config.getEnum(
 					ConfigConstants.CONFIG_PULL_SECTION, null,
 					ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE);
 		}
@@ -549,7 +549,7 @@ private FastForwardMode getFastForwardMode() {
 		Config config = repo.getConfig();
 		Merge ffMode = config.getEnum(Merge.values(),
 				ConfigConstants.CONFIG_PULL_SECTION, null,
-				ConfigConstants.CONFIG_KEY_FF, null);
+				ConfigConstants.CONFIG_KEY_FF);
 		return ffMode != null ? FastForwardMode.valueOf(ffMode) : null;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java
new file mode 100644
index 0000000..d44fb5f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.blame.cache;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Keeps the blame information for a path at certain commit.
+ * <p>
+ * If there is a result, it covers the whole file at that revision
+ *
+ * @since 7.2
+ */
+public interface BlameCache {
+	/**
+	 * Gets the blame of a path at a given commit if available.
+	 * <p>
+	 * Since this cache is used in blame calculation, this get() method should
+	 * only retrieve the cache value, and not re-trigger blame calculation. In
+	 * other words, this acts as "getIfPresent", and not "computeIfAbsent".
+	 *
+	 * @param repo
+	 *            repository containing the commit
+	 * @param commitId
+	 *            we are looking at the file in this revision
+	 * @param path
+	 *            path a file in the repo
+	 *
+	 * @return the blame of a path at a given commit or null if not in cache
+	 * @throws IOException
+	 *             error retrieving/parsing values from storage
+	 */
+	List<CacheRegion> get(Repository repo, ObjectId commitId, String path)
+			throws IOException;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java
new file mode 100644
index 0000000..6aa4eef
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.blame.cache;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Region of the blame of a file.
+ * <p>
+ * Usually all parameters are non-null, except when the Region was created
+ * to fill an unblamed gap (to cover for bugs in the calculation). In that
+ * case, path, commit and author will be null.
+ *
+ * @since 7.2
+ **/
+public class CacheRegion implements Comparable<CacheRegion> {
+	private final String sourcePath;
+
+	private final ObjectId sourceCommit;
+
+	private final int end;
+
+	private final int start;
+
+	/**
+	 * A blamed portion of a file
+	 *
+	 * @param path
+	 *            location of the file
+	 * @param commit
+	 *            commit that is modifying this region
+	 * @param start
+	 *            first line of this region (inclusive)
+	 * @param end
+	 *            last line of this region (non-inclusive!)
+	 */
+	public CacheRegion(String path, ObjectId commit,
+			int start, int end) {
+		allOrNoneNull(path, commit);
+		this.sourcePath = path;
+		this.sourceCommit = commit;
+		this.start = start;
+		this.end = end;
+	}
+
+	/**
+	 * First line of this region. Starting by 0, inclusive
+	 *
+	 * @return first line of this region.
+	 */
+	public int getStart() {
+		return start;
+	}
+
+	/**
+	 * One after last line in this region (or: last line non-inclusive)
+	 *
+	 * @return one after last line in this region.
+	 */
+	public int getEnd() {
+		return end;
+	}
+
+
+	/**
+	 * Path of the file this region belongs to
+	 *
+	 * @return path in the repo/commit
+	 */
+	public String getSourcePath() {
+		return sourcePath;
+	}
+
+	/**
+	 * Commit this region belongs to
+	 *
+	 * @return commit for this region
+	 */
+	public ObjectId getSourceCommit() {
+		return sourceCommit;
+	}
+
+	@Override
+	public int compareTo(CacheRegion o) {
+		return start - o.start;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder sb = new StringBuilder();
+		if (sourceCommit != null) {
+			sb.append(sourceCommit.name(), 0, 7).append(' ')
+					.append(" (")
+					.append(sourcePath).append(')');
+		} else {
+			sb.append("<unblamed region>");
+		}
+		sb.append(' ').append("start=").append(start).append(", count=")
+				.append(end - start);
+		return sb.toString();
+	}
+
+	private static void allOrNoneNull(String path, ObjectId commit) {
+		if (path != null && commit != null) {
+			return;
+		}
+
+		if (path == null && commit == null) {
+			return;
+		}
+		throw new IllegalArgumentException(String.format(
+				"expected all null or none: %s, %s", path, commit));
+	}
+}
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 34dba0b..c650d6e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -1037,7 +1037,12 @@ private void updateSmudgedEntries() throws IOException {
 		}
 	}
 
-	enum DirCacheVersion implements ConfigEnum {
+	/**
+	 * DirCache versions
+	 *
+	 * @since 7.2
+	 */
+	public enum DirCacheVersion implements ConfigEnum {
 
 		/** Minimum index version on-disk format that we support. */
 		DIRC_VERSION_MINIMUM(2),
@@ -1060,6 +1065,9 @@ private DirCacheVersion(int versionCode) {
 			this.version = versionCode;
 		}
 
+		/**
+		 * @return the version code for this version
+		 */
 		public int getVersionCode() {
 			return version;
 		}
@@ -1078,6 +1086,13 @@ public boolean matchConfigValue(String in) {
 			}
 		}
 
+		/**
+		 * Create DirCacheVersion from integer value of the version code.
+		 *
+		 * @param val
+		 *            integer value of the version code.
+		 * @return the DirCacheVersion instance of the version code.
+		 */
 		public static DirCacheVersion fromInt(int val) {
 			for (DirCacheVersion v : DirCacheVersion.values()) {
 				if (val == v.getVersionCode()) {
@@ -1098,9 +1113,8 @@ 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,
+			indexVersion = cfg.getEnum(ConfigConstants.CONFIG_INDEX_SECTION,
+					null, ConfigConstants.CONFIG_KEY_VERSION,
 					manyFiles ? DirCacheVersion.DIRC_VERSION_PATHCOMPRESS
 							: DirCacheVersion.DIRC_VERSION_EXTENDED);
 			skipHash = cfg.getBoolean(ConfigConstants.CONFIG_INDEX_SECTION,
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 2d9d2c5..d7fe456 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -295,6 +295,7 @@ public static JGitText get() {
 	/***/ public String deleteTagUnexpectedResult;
 	/***/ public String deletingBranches;
 	/***/ public String deletingNotSupported;
+	/***/ public String deprecatedTrustFolderStat;
 	/***/ public String depthMustBeAt1;
 	/***/ public String depthWithUnshallow;
 	/***/ public String destinationIsNotAWildcard;
@@ -493,6 +494,7 @@ public static JGitText get() {
 	/***/ public String invalidTimeUnitValue2;
 	/***/ public String invalidTimeUnitValue3;
 	/***/ public String invalidTreeZeroLengthName;
+	/***/ public String invalidTrustStat;
 	/***/ public String invalidURL;
 	/***/ public String invalidWildcards;
 	/***/ public String invalidRefSpec;
@@ -557,6 +559,8 @@ public static JGitText get() {
 	/***/ public String month;
 	/***/ public String months;
 	/***/ public String monthsAgo;
+	/***/ public String multiPackIndexUnexpectedSize;
+	/***/ public String multiPackIndexWritingCancelled;
 	/***/ public String multipleMergeBasesFor;
 	/***/ public String nameMustNotBeNullOrEmpty;
 	/***/ public String need2Arguments;
@@ -645,6 +649,7 @@ public static JGitText get() {
 	/***/ public String personIdentEmailNonNull;
 	/***/ public String personIdentNameNonNull;
 	/***/ public String postCommitHookFailed;
+	/***/ public String precedenceTrustConfig;
 	/***/ public String prefixRemote;
 	/***/ public String problemWithResolvingPushRefSpecsLocally;
 	/***/ public String progressMonUploading;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java
index b4bb2a9..909b3e3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java
@@ -26,8 +26,9 @@
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.util.FileUtils;
@@ -49,13 +50,13 @@ class LooseObjects {
 	 * Maximum number of attempts to read a loose object for which a stale file
 	 * handle exception is thrown
 	 */
-	private final static int MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS = 5;
+	private final static int MAX_STALE_READ_RETRIES = 5;
 
 	private final File directory;
 
 	private final UnpackedObjectCache unpackedObjectCache;
 
-	private final boolean trustFolderStat;
+	private final TrustStat trustLooseObjectStat;
 
 	/**
 	 * Initialize a reference to an on-disk object directory.
@@ -68,9 +69,8 @@ class LooseObjects {
 	LooseObjects(Config config, File dir) {
 		directory = dir;
 		unpackedObjectCache = new UnpackedObjectCache();
-		trustFolderStat = config.getBoolean(
-				ConfigConstants.CONFIG_CORE_SECTION,
-				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
+		trustLooseObjectStat = config.get(CoreConfig.KEY)
+				.getTrustLooseObjectStat();
 	}
 
 	/**
@@ -108,7 +108,8 @@ boolean hasCached(AnyObjectId id) {
 	 */
 	boolean has(AnyObjectId objectId) {
 		boolean exists = hasWithoutRefresh(objectId);
-		if (trustFolderStat || exists) {
+		if (trustLooseObjectStat == TrustStat.ALWAYS
+				|| exists) {
 			return exists;
 		}
 		try (InputStream stream = Files.newInputStream(directory.toPath())) {
@@ -163,13 +164,31 @@ boolean resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
 	}
 
 	ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException {
-		int readAttempts = 0;
-		while (readAttempts < MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS) {
-			readAttempts++;
-			File path = fileFor(id);
-			if (trustFolderStat && !path.exists()) {
+		File path = fileFor(id);
+		for (int retries = 0; retries < MAX_STALE_READ_RETRIES; retries++) {
+			boolean reload = true;
+			switch (trustLooseObjectStat) {
+			case NEVER:
 				break;
+			case AFTER_OPEN:
+				try (InputStream stream = Files
+						.newInputStream(path.getParentFile().toPath())) {
+					// open the loose object's fanout directory to refresh
+					// attributes (on some NFS clients)
+				} catch (FileNotFoundException | NoSuchFileException e) {
+					// ignore
+				}
+				//$FALL-THROUGH$
+			case ALWAYS:
+				if (!path.exists()) {
+					reload = false;
+				}
+				break;
+			case INHERIT:
+				// only used in CoreConfig internally
+				throw new IllegalStateException();
 			}
+			if (reload) {
 			try {
 				return getObjectLoader(curs, path, id);
 			} catch (FileNotFoundException noFile) {
@@ -183,9 +202,10 @@ ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException {
 				}
 				if (LOG.isDebugEnabled()) {
 					LOG.debug(MessageFormat.format(
-							JGitText.get().looseObjectHandleIsStale, id.name(),
-							Integer.valueOf(readAttempts), Integer.valueOf(
-									MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS)));
+								JGitText.get().looseObjectHandleIsStale,
+								id.name(), Integer.valueOf(retries),
+								Integer.valueOf(MAX_STALE_READ_RETRIES)));
+					}
 				}
 			}
 		}
@@ -211,7 +231,7 @@ ObjectLoader getObjectLoader(WindowCursor curs, File path, AnyObjectId id)
 		try {
 			return getObjectLoaderWithoutRefresh(curs, path, id);
 		} catch (FileNotFoundException e) {
-			if (trustFolderStat) {
+			if (trustLooseObjectStat == TrustStat.ALWAYS) {
 				throw e;
 			}
 			try (InputStream stream = Files
@@ -248,7 +268,7 @@ long getSize(WindowCursor curs, AnyObjectId id) throws IOException {
 			return getSizeWithoutRefresh(curs, id);
 		} catch (FileNotFoundException noFile) {
 			try {
-				if (trustFolderStat) {
+				if (trustLooseObjectStat == TrustStat.ALWAYS) {
 					throw noFile;
 				}
 				try (InputStream stream = Files
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
index e31126f..ef877d8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
@@ -17,6 +17,8 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -42,7 +44,8 @@
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.CoreConfig;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.util.FileUtils;
@@ -72,7 +75,7 @@ class PackDirectory {
 
 	private final AtomicReference<PackList> packList;
 
-	private final boolean trustFolderStat;
+	private final TrustStat trustPackStat;
 
 	/**
 	 * Initialize a reference to an on-disk 'pack' directory.
@@ -86,14 +89,7 @@ class PackDirectory {
 		this.config = config;
 		this.directory = directory;
 		packList = new AtomicReference<>(NO_PACKS);
-
-		// Whether to trust the pack folder's modification time. If set to false
-		// we will always scan the .git/objects/pack folder to check for new
-		// pack files. If set to true (default) we use the folder's size,
-		// modification time, and key (inode) and assume that no new pack files
-		// can be in this folder if these attributes have not changed.
-		trustFolderStat = config.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
-				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
+		trustPackStat = config.get(CoreConfig.KEY).getTrustPackStat();
 	}
 
 	/**
@@ -313,38 +309,42 @@ private int checkRescanPackThreshold(int retries, PackMismatchException e)
 	}
 
 	private void handlePackError(IOException e, Pack p) {
-		String warnTmpl = null;
+		String warnTemplate = null;
+		String debugTemplate = null;
 		int transientErrorCount = 0;
-		String errTmpl = JGitText.get().exceptionWhileReadingPack;
+		String errorTemplate = JGitText.get().exceptionWhileReadingPack;
 		if ((e instanceof CorruptObjectException)
 				|| (e instanceof PackInvalidException)) {
-			warnTmpl = JGitText.get().corruptPack;
-			LOG.warn(MessageFormat.format(warnTmpl,
+			warnTemplate = JGitText.get().corruptPack;
+			LOG.warn(MessageFormat.format(warnTemplate,
 					p.getPackFile().getAbsolutePath()), e);
 			// Assume the pack is corrupted, and remove it from the list.
 			remove(p);
 		} else if (e instanceof FileNotFoundException) {
 			if (p.getPackFile().exists()) {
-				errTmpl = JGitText.get().packInaccessible;
+				errorTemplate = JGitText.get().packInaccessible;
 				transientErrorCount = p.incrementTransientErrorCount();
 			} else {
-				warnTmpl = JGitText.get().packWasDeleted;
+				debugTemplate = JGitText.get().packWasDeleted;
 				remove(p);
 			}
 		} else if (FileUtils.isStaleFileHandleInCausalChain(e)) {
-			warnTmpl = JGitText.get().packHandleIsStale;
+			warnTemplate = JGitText.get().packHandleIsStale;
 			remove(p);
 		} else {
 			transientErrorCount = p.incrementTransientErrorCount();
 		}
-		if (warnTmpl != null) {
-			LOG.warn(MessageFormat.format(warnTmpl,
+		if (warnTemplate != null) {
+			LOG.warn(MessageFormat.format(warnTemplate,
 					p.getPackFile().getAbsolutePath()), e);
+		} else if (debugTemplate != null) {
+			LOG.debug(MessageFormat.format(debugTemplate,
+				p.getPackFile().getAbsolutePath()), e);
 		} else {
 			if (doLogExponentialBackoff(transientErrorCount)) {
 				// Don't remove the pack from the list, as the error may be
 				// transient.
-				LOG.error(MessageFormat.format(errTmpl,
+				LOG.error(MessageFormat.format(errorTemplate,
 						p.getPackFile().getAbsolutePath(),
 						Integer.valueOf(transientErrorCount)), e);
 			}
@@ -361,8 +361,26 @@ private boolean doLogExponentialBackoff(int n) {
 	}
 
 	boolean searchPacksAgain(PackList old) {
-		return (!trustFolderStat || old.snapshot.isModified(directory))
-				&& old != scanPacks(old);
+		switch (trustPackStat) {
+		case NEVER:
+			break;
+		case AFTER_OPEN:
+			try (InputStream stream = Files
+					.newInputStream(directory.toPath())) {
+				// open the pack directory to refresh attributes (on some NFS clients)
+			} catch (IOException e) {
+				// ignore
+			}
+			//$FALL-THROUGH$
+		case ALWAYS:
+			if (!old.snapshot.isModified(directory)) {
+				return false;
+			}
+			break;
+		case INHERIT:
+			// only used in CoreConfig internally
+		}
+		return old != scanPacks(old);
 	}
 
 	void insert(Pack pack) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
index 7189ce2..b3e4efb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
@@ -286,7 +286,7 @@ long findCRC32(AnyObjectId objId)
 	 *             the index cannot be read.
 	 */
 	void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
-			int matchLimit) throws IOException;
+				 int matchLimit) throws IOException;
 
 	/**
 	 * Get pack checksum
@@ -304,6 +304,7 @@ void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
 	class MutableEntry {
 		/** Buffer of the ObjectId visited by the EntriesIterator. */
 		final MutableObjectId idBuffer = new MutableObjectId();
+
 		/** Offset into the packfile of the current object. */
 		long offset;
 
@@ -345,6 +346,34 @@ public MutableEntry cloneEntry() {
 			r.offset = offset;
 			return r;
 		}
+
+		/**
+		 * Similar to {@link Comparable#compareTo(Object)}, using only the
+		 * object id in the entry.
+		 *
+		 * @param other
+		 *            Another mutable entry (probably from another index)
+		 *
+		 * @return a negative integer, zero, or a positive integer as this
+		 *         object is less than, equal to, or greater than the specified
+		 *         object.
+		 */
+		public int compareBySha1To(MutableEntry other) {
+			return idBuffer.compareTo(other.idBuffer);
+		}
+
+		/**
+		 * Copy the current ObjectId to dest
+		 * <p>
+		 * Like {@link #toObjectId()}, but reusing the destination instead of
+		 * creating a new ObjectId instance.
+		 *
+		 * @param dest
+		 *            destination for the object id
+		 */
+		public void copyOidTo(MutableObjectId dest) {
+			dest.fromObjectId(idBuffer);
+		}
 	}
 
 	/**
@@ -368,7 +397,6 @@ protected EntriesIterator(long objectCount) {
 			this.objectCount = objectCount;
 		}
 
-
 		@Override
 		public boolean hasNext() {
 			return returnedNumber < objectCount;
@@ -393,7 +421,6 @@ public MutableEntry next() {
 		 */
 		protected abstract void readNext();
 
-
 		/**
 		 * Copies to the entry an {@link ObjectId} from the int buffer and
 		 * position idx
@@ -423,7 +450,8 @@ protected void setIdBuffer(byte[] raw, int idx) {
 		/**
 		 * Sets the {@code offset} to the entry
 		 *
-		 * @param offset the offset in the pack file
+		 * @param offset
+		 *            the offset in the pack file
 		 */
 		protected void setOffset(long offset) {
 			entry.offset = offset;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index 3399651..05f1ef5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -64,10 +64,9 @@
 import org.eclipse.jgit.errors.ObjectWritingException;
 import org.eclipse.jgit.events.RefsChangedEvent;
 import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.CoreConfig.TrustLooseRefStat;
-import org.eclipse.jgit.lib.CoreConfig.TrustPackedRefsStat;
+import org.eclipse.jgit.lib.CoreConfig;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.ProgressMonitor;
@@ -185,11 +184,7 @@ public class RefDirectory extends RefDatabase {
 
 	private List<Integer> retrySleepMs = RETRY_SLEEP_MS;
 
-	private final boolean trustFolderStat;
-
-	private final TrustPackedRefsStat trustPackedRefsStat;
-
-	private final TrustLooseRefStat trustLooseRefStat;
+	private final CoreConfig coreConfig;
 
 	RefDirectory(RefDirectory refDb) {
 		parent = refDb.parent;
@@ -201,9 +196,7 @@ public class RefDirectory extends RefDatabase {
 		packedRefsFile = refDb.packedRefsFile;
 		looseRefs.set(refDb.looseRefs.get());
 		packedRefs.set(refDb.packedRefs.get());
-		trustFolderStat = refDb.trustFolderStat;
-		trustPackedRefsStat = refDb.trustPackedRefsStat;
-		trustLooseRefStat = refDb.trustLooseRefStat;
+		coreConfig = refDb.coreConfig;
 		inProcessPackedRefsLock = refDb.inProcessPackedRefsLock;
 	}
 
@@ -219,17 +212,7 @@ public class RefDirectory extends RefDatabase {
 
 		looseRefs.set(RefList.<LooseRef> emptyList());
 		packedRefs.set(NO_PACKED_REFS);
-		trustFolderStat = db.getConfig()
-				.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
-						ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
-		trustPackedRefsStat = db.getConfig()
-				.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_TRUST_PACKED_REFS_STAT,
-						TrustPackedRefsStat.UNSET);
-		trustLooseRefStat = db.getConfig()
-				.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_TRUST_LOOSE_REF_STAT,
-						TrustLooseRefStat.ALWAYS);
+		coreConfig = db.getConfig().get(CoreConfig.KEY);
 		inProcessPackedRefsLock = new ReentrantLock(true);
 	}
 
@@ -979,7 +962,7 @@ else if (0 <= (idx = packed.find(dst.getName())))
 	PackedRefList getPackedRefs() throws IOException {
 		final PackedRefList curList = packedRefs.get();
 
-		switch (trustPackedRefsStat) {
+		switch (coreConfig.getTrustPackedRefsStat()) {
 		case NEVER:
 			break;
 		case AFTER_OPEN:
@@ -995,12 +978,8 @@ PackedRefList getPackedRefs() throws IOException {
 				return curList;
 			}
 			break;
-		case UNSET:
-			if (trustFolderStat
-					&& !curList.snapshot.isModified(packedRefsFile)) {
-				return curList;
-			}
-			break;
+		case INHERIT:
+			// only used in CoreConfig internally
 		}
 
 		return refreshPackedRefs(curList);
@@ -1186,7 +1165,7 @@ private Ref readRef(String name, RefList<Ref> packed) throws IOException {
 	LooseRef scanRef(LooseRef ref, String name) throws IOException {
 		final File path = fileFor(name);
 
-		if (trustLooseRefStat.equals(TrustLooseRefStat.AFTER_OPEN)) {
+		if (coreConfig.getTrustLooseRefStat() == TrustStat.AFTER_OPEN) {
 			refreshPathToLooseRef(Paths.get(name));
 		}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java
new file mode 100644
index 0000000..6122a9a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+class MultiPackIndexConstants {
+	static final int MIDX_SIGNATURE = 0x4d494458; /* MIDX */
+
+	static final byte MIDX_VERSION = 1;
+
+	/**
+	 * We infer the length of object IDs (OIDs) from this value:
+	 * 
+	 * <pre>
+	 * 1 => SHA-1
+	 * 2 => SHA-256
+	 * </pre>
+	 */
+	static final byte OID_HASH_VERSION = 1;
+
+	static final int MULTIPACK_INDEX_FANOUT_SIZE = 4 * 256;
+
+	/**
+	 * First 4 bytes describe the chunk id. Value 0 is a terminating label.
+	 * Other 8 bytes provide the byte-offset in current file for chunk to start.
+	 */
+	static final int CHUNK_LOOKUP_WIDTH = 12;
+
+	/** "PNAM" chunk */
+	static final int MIDX_CHUNKID_PACKNAMES = 0x504e414d;
+
+	/** "OIDF" chunk */
+	static final int MIDX_CHUNKID_OIDFANOUT = 0x4f494446;
+
+	/** "OIDL" chunk */
+	static final int MIDX_CHUNKID_OIDLOOKUP = 0x4f49444c;
+
+	/** "OOFF" chunk */
+	static final int MIDX_CHUNKID_OBJECTOFFSETS = 0x4f4f4646;
+
+	/** "LOFF" chunk */
+	static final int MIDX_CHUNKID_LARGEOFFSETS = 0x4c4f4646;
+
+	/** "RIDX" chunk */
+	static final int MIDX_CHUNKID_REVINDEX = 0x52494458;
+
+	private MultiPackIndexConstants() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java
new file mode 100644
index 0000000..e8428b8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.NB;
+
+/**
+ * Prints a multipack index file in a human-readable format.
+ */
+@SuppressWarnings("nls")
+public class MultiPackIndexPrettyPrinter {
+
+	/**
+	 * Writes to out, in human-readable format, the multipack index in rawMidx
+	 *
+	 * @param rawMidx the bytes of a multipack index
+	 * @param out a writer
+	 */
+	public static void prettyPrint(byte[] rawMidx, PrintWriter out) {
+		// Header (12 bytes)
+		out.println("[ 0] Magic: " + new String(rawMidx, 0, 4, UTF_8));
+		out.println("[ 4] Version number: " + (int) rawMidx[4]);
+		out.println("[ 5] OID version: " + (int) rawMidx[5]);
+		int chunkCount = rawMidx[6];
+		out.println("[ 6] # of chunks: " + chunkCount);
+		out.println("[ 7] # of bases: " + (int) rawMidx[7]);
+		int numberOfPacks = NB.decodeInt32(rawMidx, 8);
+		out.println("[ 8] # of packs: " + numberOfPacks);
+
+		// Chunk lookup table
+		List<ChunkSegment> chunkSegments = new ArrayList<>();
+		int current = printChunkLookup(out, rawMidx, chunkCount, chunkSegments);
+
+		for (int i = 0; i < chunkSegments.size() - 1; i++) {
+			ChunkSegment segment = chunkSegments.get(i);
+			if (current != segment.startOffset()) {
+				throw new IllegalStateException(String.format(
+						"We are at byte %d, but segment should start at %d",
+						current, segment.startOffset()));
+			}
+			out.printf("Starting chunk: %s @ %d%n", segment.chunkName(),
+					segment.startOffset());
+			switch (segment.chunkName()) {
+				case "OIDF" -> current = printOIDF(out, rawMidx, current);
+				case "OIDL" -> current = printOIDL(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "OOFF" -> current = printOOFF(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "PNAM" -> current = printPNAM(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "RIDX" -> current = printRIDX(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				default -> {
+					out.printf(
+							"Skipping %s (don't know how to print it yet)%n",
+							segment.chunkName());
+					current = (int) chunkSegments.get(i + 1).startOffset();
+				}
+			}
+		}
+		// Checksum is a SHA-1, use ObjectId to parse it
+		out.printf("[ %d] Checksum %s%n", current,
+				ObjectId.fromRaw(rawMidx, current).name());
+		out.printf("Total size: " + (current + 20));
+	}
+
+	private static int printChunkLookup(PrintWriter out, byte[] rawMidx, int chunkCount,
+			List<ChunkSegment> chunkSegments) {
+		out.println("Starting chunk lookup @ 12");
+		int current = 12;
+		for (int i = 0; i < chunkCount; i++) {
+			String chunkName = new String(rawMidx, current, 4, UTF_8);
+			long offset = NB.decodeInt64(rawMidx, current + 4);
+			out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset);
+			current += CHUNK_LOOKUP_WIDTH;
+			chunkSegments.add(new ChunkSegment(chunkName, offset));
+		}
+		String chunkName = "0000";
+		long offset = NB.decodeInt64(rawMidx, current + 4);
+		out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset);
+		current += CHUNK_LOOKUP_WIDTH;
+		chunkSegments.add(new ChunkSegment(chunkName, offset));
+		return current;
+	}
+
+	private static int printOIDF(PrintWriter out, byte[] rawMidx, int start) {
+		int current = start;
+		for (short i = 0; i < 256; i++) {
+			out.printf("[ %d] (%02X) %d%n", current, i,
+					NB.decodeInt32(rawMidx, current));
+			current += 4;
+		}
+		return current;
+	}
+
+	private static int printOIDL(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %s%n", i,
+					ObjectId.fromRaw(rawMidx, i).name());
+			i += 20;
+		}
+		return i;
+	}
+
+	private static int printOOFF(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %d %d%n", i, NB.decodeInt32(rawMidx, i),
+					NB.decodeInt32(rawMidx, i + 4));
+			i += 8;
+		}
+		return i;
+	}
+
+	private static int printRIDX(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %d%n", i, NB.decodeInt32(rawMidx, i));
+			i += 4;
+		}
+		return (int) end;
+	}
+
+	private static int printPNAM(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int nameStart = start;
+		for (int i = start; i < end; i++) {
+			if (rawMidx[i] == 0) {
+				out
+						.println(new String(rawMidx, nameStart, i - nameStart));
+				nameStart = i + 1;
+			}
+		}
+		return (int) end;
+	}
+
+	private record ChunkSegment(String chunkName, long startOffset) {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java
new file mode 100644
index 0000000..bddf3ac
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_SIGNATURE;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_VERSION;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MULTIPACK_INDEX_FANOUT_SIZE;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.OID_HASH_VERSION;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream;
+import org.eclipse.jgit.internal.storage.midx.PackIndexMerger.MidxMutableEntry;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.util.NB;
+
+/**
+ * Writes a collection of indexes as a multipack index.
+ * <p>
+ * See <a href=
+ * "https://git-scm.com/docs/pack-format#_multi_pack_index_midx_files_have_the_following_format">multipack
+ * index format spec</a>
+ *
+ * @since 7.2
+ */
+public class MultiPackIndexWriter {
+
+	private static final int LIMIT_31_BITS = (1 << 31) - 1;
+
+	private static final int MIDX_HEADER_SIZE = 12;
+
+	/**
+	 * Writes the inputs in the multipack index format in the outputStream.
+	 *
+	 * @param monitor
+	 *            progress monitor
+	 * @param outputStream
+	 *            stream to write the multipack index file
+	 * @param inputs
+	 *            pairs of name and index for each pack to include in the
+	 *            multipack index.
+	 * @throws IOException
+	 *             Error writing to the stream
+	 */
+	public void write(ProgressMonitor monitor, OutputStream outputStream,
+			Map<String, PackIndex> inputs) throws IOException {
+		PackIndexMerger data = new PackIndexMerger(inputs);
+
+		// List of chunks in the order they need to be written
+		List<ChunkHeader> chunkHeaders = createChunkHeaders(data);
+		long expectedSize = calculateExpectedSize(chunkHeaders);
+		try (CancellableDigestOutputStream out = new CancellableDigestOutputStream(
+				monitor, outputStream)) {
+			writeHeader(out, chunkHeaders.size(), data.getPackCount());
+			writeChunkLookup(out, chunkHeaders);
+
+			WriteContext ctx = new WriteContext(out, data);
+			for (ChunkHeader chunk : chunkHeaders) {
+				chunk.writerFn.write(ctx);
+			}
+			writeCheckSum(out);
+			if (expectedSize != out.length()) {
+				throw new IllegalStateException(String.format(
+						JGitText.get().multiPackIndexUnexpectedSize,
+						Long.valueOf(expectedSize),
+						Long.valueOf(out.length())));
+			}
+		} catch (InterruptedIOException e) {
+			throw new IOException(JGitText.get().multiPackIndexWritingCancelled,
+					e);
+		}
+	}
+
+	private static long calculateExpectedSize(List<ChunkHeader> chunks) {
+		int chunkLookup = (chunks.size() + 1) * CHUNK_LOOKUP_WIDTH;
+		long chunkContent = chunks.stream().mapToLong(c -> c.size).sum();
+		return /* header */ 12 + chunkLookup + chunkContent + /* CRC */ 20;
+	}
+
+	private List<ChunkHeader> createChunkHeaders(PackIndexMerger data) {
+		List<ChunkHeader> chunkHeaders = new ArrayList<>();
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OIDFANOUT,
+				MULTIPACK_INDEX_FANOUT_SIZE, this::writeFanoutTable));
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OIDLOOKUP,
+				(long) data.getUniqueObjectCount() * OBJECT_ID_LENGTH,
+				this::writeOidLookUp));
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OBJECTOFFSETS,
+				8L * data.getUniqueObjectCount(), this::writeObjectOffsets));
+		if (data.needsLargeOffsetsChunk()) {
+			chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_LARGEOFFSETS,
+					8L * data.getOffsetsOver31BitsCount(),
+					this::writeObjectLargeOffsets));
+		}
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_REVINDEX,
+				4L * data.getUniqueObjectCount(), this::writeRidx));
+
+		int packNamesSize = data.getPackNames().stream()
+				.mapToInt(String::length).map(i -> i + 1 /* null at the end */)
+				.sum();
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_PACKNAMES, packNamesSize,
+				this::writePackfileNames));
+		return chunkHeaders;
+	}
+
+	/**
+	 * Write the first 12 bytes of the multipack index.
+	 * <p>
+	 * These bytes include things like magic number, version, number of
+	 * chunks...
+	 *
+	 * @param out
+	 *            output stream to write
+	 * @param numChunks
+	 *            number of chunks this multipack index is going to have
+	 * @param packCount
+	 *            number of packs covered by this multipack index
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeHeader(CancellableDigestOutputStream out, int numChunks,
+			int packCount) throws IOException {
+		byte[] headerBuffer = new byte[MIDX_HEADER_SIZE];
+		NB.encodeInt32(headerBuffer, 0, MIDX_SIGNATURE);
+		byte[] buff = { MIDX_VERSION, OID_HASH_VERSION, (byte) numChunks,
+				(byte) 0 };
+		System.arraycopy(buff, 0, headerBuffer, 4, 4);
+		NB.encodeInt32(headerBuffer, 8, packCount);
+		out.write(headerBuffer, 0, headerBuffer.length);
+		out.flush();
+	}
+
+	/**
+	 * Write a table of "chunkId, start-offset", with a special value "0,
+	 * end-of-previous_chunk", to mark the end.
+	 *
+	 * @param out
+	 *            output stream to write
+	 * @param chunkHeaders
+	 *            list of chunks in the order they are expected to be written
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeChunkLookup(CancellableDigestOutputStream out,
+			List<ChunkHeader> chunkHeaders) throws IOException {
+
+		// first chunk will start at header + this lookup block
+		long chunkStart = MIDX_HEADER_SIZE
+				+ (long) (chunkHeaders.size() + 1) * CHUNK_LOOKUP_WIDTH;
+		byte[] chunkEntry = new byte[CHUNK_LOOKUP_WIDTH];
+		for (ChunkHeader chunkHeader : chunkHeaders) {
+			NB.encodeInt32(chunkEntry, 0, chunkHeader.chunkId);
+			NB.encodeInt64(chunkEntry, 4, chunkStart);
+			out.write(chunkEntry);
+			chunkStart += chunkHeader.size;
+		}
+		// Terminating label for the block
+		// (chunkid 0, offset where the next block would start)
+		NB.encodeInt32(chunkEntry, 0, 0);
+		NB.encodeInt64(chunkEntry, 4, chunkStart);
+		out.write(chunkEntry);
+	}
+
+	/**
+	 * Write the fanout table for the object ids
+	 * <p>
+	 * Table with 256 entries (one byte), where the ith entry, F[i], stores the
+	 * number of OIDs with first byte at most i. Thus, F[255] stores the total
+	 * number of objects.
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+
+	private void writeFanoutTable(WriteContext ctx) throws IOException {
+		byte[] tmp = new byte[4];
+		int[] fanout = new int[256];
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			fanout[e.getObjectId().getFirstByte() & 0xff]++;
+		}
+		for (int i = 1; i < fanout.length; i++) {
+			fanout[i] += fanout[i - 1];
+		}
+		for (int n : fanout) {
+			NB.encodeInt32(tmp, 0, n);
+			ctx.out.write(tmp, 0, 4);
+		}
+	}
+
+	/**
+	 * Write the OID lookup chunk
+	 * <p>
+	 * A list of OIDs in sha1 order.
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeOidLookUp(WriteContext ctx) throws IOException {
+		byte[] tmp = new byte[OBJECT_ID_LENGTH];
+
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			e.getObjectId().copyRawTo(tmp, 0);
+			ctx.out.write(tmp, 0, OBJECT_ID_LENGTH);
+		}
+	}
+
+	/**
+	 * Write the object offsets chunk
+	 * <p>
+	 * A list of offsets, parallel to the list of OIDs. If the offset is too
+	 * large (see {@link #fitsIn31bits(long)}), this contains the position in
+	 * the large offsets list (marked with a 1 in the most significant bit).
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeObjectOffsets(WriteContext ctx) throws IOException {
+		byte[] entry = new byte[8];
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			NB.encodeInt32(entry, 0, e.getPackId());
+			if (!ctx.data.needsLargeOffsetsChunk()
+					|| fitsIn31bits(e.getOffset())) {
+				NB.encodeInt32(entry, 4, (int) e.getOffset());
+			} else {
+				int offloadedPosition = ctx.largeOffsets.append(e.getOffset());
+				NB.encodeInt32(entry, 4, offloadedPosition | (1 << 31));
+			}
+			ctx.out.write(entry);
+		}
+	}
+
+	/**
+	 * Writes the reverse index chunk
+	 * <p>
+	 * This stores the position of the objects in the main index, ordered first
+	 * by pack and then by offset
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             erorr writing to the output stream
+	 */
+	private void writeRidx(WriteContext ctx) throws IOException {
+		Map<Integer, List<OffsetPosition>> packOffsets = new HashMap<>(
+				ctx.data.getPackCount());
+		// TODO(ifrade): Brute force solution loading all offsets/packs in
+		// memory. We could also iterate reverse indexes looking up
+		// their position in the midx (and discarding if the pack doesn't
+		// match).
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		int midxPosition = 0;
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			OffsetPosition op = new OffsetPosition(e.getOffset(), midxPosition);
+			midxPosition++;
+			packOffsets.computeIfAbsent(Integer.valueOf(e.getPackId()),
+					k -> new ArrayList<>()).add(op);
+		}
+
+		for (int i = 0; i < ctx.data.getPackCount(); i++) {
+			List<OffsetPosition> offsetsForPack = packOffsets
+					.get(Integer.valueOf(i));
+			if (offsetsForPack.isEmpty()) {
+				continue;
+			}
+			offsetsForPack.sort(Comparator.comparing(OffsetPosition::offset));
+			byte[] ridxForPack = new byte[4 * offsetsForPack.size()];
+			for (int j = 0; j < offsetsForPack.size(); j++) {
+				NB.encodeInt32(ridxForPack, j * 4,
+						offsetsForPack.get(j).position);
+			}
+			ctx.out.write(ridxForPack);
+		}
+	}
+
+	/**
+	 * Write the large offset chunk
+	 * <p>
+	 * A list of large offsets (long). The regular offset chunk will point to a
+	 * position here.
+	 *
+	 * @param ctx
+	 *            writer context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeObjectLargeOffsets(WriteContext ctx) throws IOException {
+		ctx.out.write(ctx.largeOffsets.offsets, 0,
+				ctx.largeOffsets.bytePosition);
+	}
+
+	/**
+	 * Write the list of packfiles chunk
+	 * <p>
+	 * List of packfiles (in lexicographical order) with an \0 at the end
+	 *
+	 * @param ctx
+	 *            writer context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writePackfileNames(WriteContext ctx) throws IOException {
+		for (String packName : ctx.data.getPackNames()) {
+			// Spec doesn't talk about encoding.
+			ctx.out.write(packName.getBytes(StandardCharsets.UTF_8));
+			ctx.out.write(0);
+		}
+	}
+
+	/**
+	 * Write final checksum of the data written to the stream
+	 *
+	 * @param out
+	 *            output stream used to write
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeCheckSum(CancellableDigestOutputStream out)
+			throws IOException {
+		out.write(out.getDigest());
+		out.flush();
+	}
+
+
+	private record OffsetPosition(long offset, int position) {
+	}
+
+	/**
+	 * If there is at least one offset value larger than 2^32-1, then the large
+	 * offset chunk must exist, and offsets larger than 2^31-1 must be stored in
+	 * it instead
+	 *
+	 * @param offset object offset
+	 *
+	 * @return true if the offset fits in 31 bits
+	 */
+	private static boolean fitsIn31bits(long offset) {
+		return offset <= LIMIT_31_BITS;
+	}
+
+	private static class LargeOffsets {
+		private final byte[] offsets;
+
+		private int bytePosition;
+
+		LargeOffsets(int largeOffsetsCount) {
+			offsets = new byte[largeOffsetsCount * 8];
+			bytePosition = 0;
+		}
+
+		/**
+		 * Add an offset to the large offset chunk
+		 *
+		 * @param largeOffset
+		 *            a large offset
+		 * @return the position of the just inserted offset (as in number of
+		 *         offsets, NOT in bytes)
+		 */
+		int append(long largeOffset) {
+			int at = bytePosition;
+			NB.encodeInt64(offsets, at, largeOffset);
+			bytePosition += 8;
+			return at / 8;
+		}
+	}
+
+	private record ChunkHeader(int chunkId, long size, ChunkWriter writerFn) {
+	}
+
+	@FunctionalInterface
+	private interface ChunkWriter {
+		void write(WriteContext ctx) throws IOException;
+	}
+
+	private static class WriteContext {
+		final CancellableDigestOutputStream out;
+
+		final PackIndexMerger data;
+
+		final LargeOffsets largeOffsets;
+
+		WriteContext(CancellableDigestOutputStream out, PackIndexMerger data) {
+			this.out = out;
+			this.data = data;
+			this.largeOffsets = new LargeOffsets(
+					data.getOffsetsOver31BitsCount());
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java
new file mode 100644
index 0000000..89814af
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.internal.storage.midx;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.MutableObjectId;
+
+/**
+ * Collect the stats and offers an iterator over the union of n-pack indexes.
+ * <p>
+ * The multipack index is a list of (sha1, packid, offset) ordered by sha1. We
+ * can build it from the individual pack indexes (sha1, offset) ordered by sha1,
+ * with a simple merge ignoring duplicates.
+ * <p>
+ * This class encapsulates the merging logic and precalculates the stats that
+ * the index needs (like total count of objects). To limit memory consumption,
+ * it does the merge as it goes during the iteration and iterators use mutable
+ * entries. The stats of the combined index are calculated in an iteration at
+ * construction time.
+ */
+class PackIndexMerger {
+
+	private static final int LIMIT_31_BITS = (1 << 31) - 1;
+
+	private static final long LIMIT_32_BITS = (1L << 32) - 1;
+
+	/**
+	 * Object returned by the iterator.
+	 * <p>
+	 * The iterator returns (on each next()) the same instance with different
+	 * values, to avoid allocating many short-lived objects. Callers should not
+	 * keep a reference to that returned value.
+	 */
+	static class MidxMutableEntry {
+		// The object id
+		private final MutableObjectId oid = new MutableObjectId();
+
+		// Position of the pack in the ordered list of pack in this merger
+		private int packId;
+
+		// Offset in its pack
+		private long offset;
+
+		public AnyObjectId getObjectId() {
+			return oid;
+		}
+
+		public int getPackId() {
+			return packId;
+		}
+
+		public long getOffset() {
+			return offset;
+		}
+
+		/**
+		 * Copy values from another mutable entry
+		 *
+		 * @param packId
+		 *            packId
+		 * @param other
+		 *            another mutable entry
+		 */
+		private void fill(int packId, PackIndex.MutableEntry other) {
+			other.copyOidTo(oid);
+			this.packId = packId;
+			this.offset = other.getOffset();
+		}
+	}
+
+	private final List<String> packNames;
+
+	private final List<PackIndex> indexes;
+
+	private final boolean needsLargeOffsetsChunk;
+
+	private final int offsetsOver31BitsCount;
+
+	private final int uniqueObjectCount;
+
+	PackIndexMerger(Map<String, PackIndex> packs) {
+		this.packNames = packs.keySet().stream().sorted()
+				.collect(Collectors.toUnmodifiableList());
+
+		this.indexes = packNames.stream().map(packs::get)
+				.collect(Collectors.toUnmodifiableList());
+
+		// Iterate for duplicates
+		int objectCount = 0;
+		boolean hasLargeOffsets = false;
+		int over31bits = 0;
+		MutableObjectId lastSeen = new MutableObjectId();
+		MultiIndexIterator it = new MultiIndexIterator(indexes);
+		while (it.hasNext()) {
+			MidxMutableEntry entry = it.next();
+			if (lastSeen.equals(entry.oid)) {
+				continue;
+			}
+			// If there is at least one offset value larger than 2^32-1, then
+			// the large offset chunk must exist, and offsets larger than
+			// 2^31-1 must be stored in it instead
+			if (entry.offset > LIMIT_32_BITS) {
+				hasLargeOffsets = true;
+			}
+			if (entry.offset > LIMIT_31_BITS) {
+				over31bits++;
+			}
+
+			lastSeen.fromObjectId(entry.oid);
+			objectCount++;
+		}
+		uniqueObjectCount = objectCount;
+		offsetsOver31BitsCount = over31bits;
+		needsLargeOffsetsChunk = hasLargeOffsets;
+	}
+
+	/**
+	 * Object count of the merged index (i.e. without duplicates)
+	 *
+	 * @return object count of the merged index
+	 */
+	int getUniqueObjectCount() {
+		return uniqueObjectCount;
+	}
+
+	/**
+	 * If any object in any of the indexes has an offset over 2^32-1
+	 *
+	 * @return true if there is any object with offset > 2^32 -1
+	 */
+	boolean needsLargeOffsetsChunk() {
+		return needsLargeOffsetsChunk;
+	}
+
+	/**
+	 * How many object have offsets over 2^31-1
+	 * <p>
+	 * Per multipack index spec, IF there is large offset chunk, all this
+	 * offsets should be there.
+	 *
+	 * @return number of objects with offsets over 2^31-1
+	 */
+	int getOffsetsOver31BitsCount() {
+		return offsetsOver31BitsCount;
+	}
+
+	/**
+	 * List of pack names in alphabetical order.
+	 * <p>
+	 * Order matters: In case of duplicates, the multipack index prefers the
+	 * first package with it. This is in the same order we are using to
+	 * prioritize duplicates.
+	 *
+	 * @return List of pack names, in the order used by the merge.
+	 */
+	List<String> getPackNames() {
+		return packNames;
+	}
+
+	/**
+	 * How many packs are being merged
+	 *
+	 * @return count of packs merged
+	 */
+	int getPackCount() {
+		return packNames.size();
+	}
+
+	/**
+	 * Iterator over the merged indexes in sha1 order without duplicates
+	 * <p>
+	 * The returned entry in the iterator is mutable, callers should NOT keep a
+	 * reference to it.
+	 *
+	 * @return an iterator in sha1 order without duplicates.
+	 */
+	Iterator<MidxMutableEntry> bySha1Iterator() {
+		return new DedupMultiIndexIterator(new MultiIndexIterator(indexes),
+				getUniqueObjectCount());
+	}
+
+	/**
+	 * For testing. Iterate all entries, not skipping duplicates (stable order)
+	 *
+	 * @return an iterator of all objects in sha1 order, including duplicates.
+	 */
+	Iterator<MidxMutableEntry> rawIterator() {
+		return new MultiIndexIterator(indexes);
+	}
+
+	/**
+	 * Iterator over n-indexes in ObjectId order.
+	 * <p>
+	 * It returns duplicates if the same object id is in different indexes. Wrap
+	 * it with {@link DedupMultiIndexIterator (Iterator, int)} to avoid
+	 * duplicates.
+	 */
+	private static final class MultiIndexIterator
+			implements Iterator<MidxMutableEntry> {
+
+		private final List<PackIndexPeekIterator> indexIterators;
+
+		private final MidxMutableEntry mutableEntry = new MidxMutableEntry();
+
+		MultiIndexIterator(List<PackIndex> indexes) {
+			this.indexIterators = new ArrayList<>(indexes.size());
+			for (int i = 0; i < indexes.size(); i++) {
+				PackIndexPeekIterator it = new PackIndexPeekIterator(i,
+						indexes.get(i));
+				// Position in the first element
+				if (it.next() != null) {
+					indexIterators.add(it);
+				}
+			}
+		}
+
+		@Override
+		public boolean hasNext() {
+			return !indexIterators.isEmpty();
+		}
+
+		@Override
+		public MidxMutableEntry next() {
+			PackIndexPeekIterator winner = null;
+			for (int index = 0; index < indexIterators.size(); index++) {
+				PackIndexPeekIterator current = indexIterators.get(index);
+				if (winner == null
+						|| current.peek().compareBySha1To(winner.peek()) < 0) {
+					winner = current;
+				}
+			}
+
+			if (winner == null) {
+				throw new NoSuchElementException();
+			}
+
+			mutableEntry.fill(winner.getPackId(), winner.peek());
+			if (winner.next() == null) {
+				indexIterators.remove(winner);
+			};
+			return mutableEntry;
+		}
+	}
+
+	private static class DedupMultiIndexIterator
+			implements Iterator<MidxMutableEntry> {
+		private final MultiIndexIterator src;
+
+		private int remaining;
+
+		private final MutableObjectId lastOid = new MutableObjectId();
+
+		DedupMultiIndexIterator(MultiIndexIterator src, int totalCount) {
+			this.src = src;
+			this.remaining = totalCount;
+		}
+
+		@Override
+		public boolean hasNext() {
+			return remaining > 0;
+		}
+
+		@Override
+		public MidxMutableEntry next() {
+			MidxMutableEntry next = src.next();
+			while (next != null && lastOid.equals(next.oid)) {
+				next = src.next();
+			}
+
+			if (next == null) {
+				throw new NoSuchElementException();
+			}
+
+			lastOid.fromObjectId(next.oid);
+			remaining--;
+			return next;
+		}
+	}
+
+	/**
+	 * Convenience around the PackIndex iterator to read the current value
+	 * multiple times without consuming it.
+	 * <p>
+	 * This is used to merge indexes in the multipack index, where we need to
+	 * compare the current value between indexes multiple times to find the
+	 * next.
+	 * <p>
+	 * We could also implement this keeping the position (int) and
+	 * MutableEntry#getObjectId, but that would create an ObjectId per entry.
+	 * This implementation reuses the MutableEntry and avoid instantiations.
+	 */
+	// Visible for testing
+	static class PackIndexPeekIterator {
+		private final Iterator<PackIndex.MutableEntry> it;
+
+		private final int packId;
+
+		PackIndex.MutableEntry current;
+
+		PackIndexPeekIterator(int packId, PackIndex index) {
+			it = index.iterator();
+			this.packId = packId;
+		}
+
+		PackIndex.MutableEntry next() {
+			if (it.hasNext()) {
+				current = it.next();
+			} else {
+				current = null;
+			}
+			return current;
+		}
+
+		PackIndex.MutableEntry peek() {
+			return current;
+		}
+
+		int getPackId() {
+			return packId;
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java
index e15c7af..7921052 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java
@@ -187,8 +187,7 @@ public boolean isRebase() {
 	 * @since 4.5
 	 */
 	public BranchRebaseMode getRebaseMode() {
-		return config.getEnum(BranchRebaseMode.values(),
-				ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
+		return config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
 				ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE);
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
index f701a41..b1ba5df 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
@@ -119,7 +119,7 @@ private CommitConfig(Config rc) {
 		if (!StringUtils.isEmptyOrNull(comment)) {
 			if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$
 				autoCommentChar = true;
-			} else {
+			} else if (comment != null) {
 				char first = comment.charAt(0);
 				if (first > ' ' && first < 127) {
 					commentCharacter = first;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
index 07c5fa4..345cb22 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
@@ -34,6 +34,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.events.ConfigChangedEvent;
 import org.eclipse.jgit.events.ConfigChangedListener;
@@ -254,9 +255,8 @@ static String escapeSubsection(String x) {
 	 *            default value to return if no value was present.
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
-	public int getInt(final String section, final String name,
-			final int defaultValue) {
-		return typedGetter.getInt(this, section, null, name, defaultValue);
+	public int getInt(String section, String name, int defaultValue) {
+		return getInt(section, null, name, defaultValue);
 	}
 
 	/**
@@ -264,6 +264,23 @@ public int getInt(final String section, final String name,
 	 *
 	 * @param section
 	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getInt(String section, String name) {
+		return getInt(section, null, name);
+	}
+
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
 	 * @param subsection
 	 *            subsection name, such a remote or branch name.
 	 * @param name
@@ -272,10 +289,30 @@ public int getInt(final String section, final String name,
 	 *            default value to return if no value was present.
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
-	public int getInt(final String section, String subsection,
-			final String name, final int defaultValue) {
+	public int getInt(String section, String subsection, String name,
+			int defaultValue) {
+		Integer v = typedGetter.getInt(this, section, subsection, name,
+				Integer.valueOf(defaultValue));
+		return v == null ? defaultValue : v.intValue();
+	}
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getInt(String section, String subsection, String name) {
 		return typedGetter.getInt(this, section, subsection, name,
-				defaultValue);
+				null);
 	}
 
 	/**
@@ -297,8 +334,30 @@ public int getInt(final String section, String subsection,
 	 */
 	public int getIntInRange(String section, String name, int minValue,
 			int maxValue, int defaultValue) {
-		return typedGetter.getIntInRange(this, section, null, name, minValue,
-				maxValue, defaultValue);
+		return getIntInRange(section, null, name,
+				minValue, maxValue, defaultValue);
+	}
+
+	/**
+	 * Obtain an integer value from the configuration which must be inside given
+	 * range.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @param minValue
+	 *            minimum value
+	 * @param maxValue
+	 *            maximum value
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getIntInRange(String section, String name, int minValue,
+			int maxValue) {
+		return getIntInRange(section, null, name, minValue, maxValue);
 	}
 
 	/**
@@ -322,8 +381,34 @@ public int getIntInRange(String section, String name, int minValue,
 	 */
 	public int getIntInRange(String section, String subsection, String name,
 			int minValue, int maxValue, int defaultValue) {
+		Integer v = typedGetter.getIntInRange(this, section, subsection, name,
+				minValue, maxValue, Integer.valueOf(defaultValue));
+		return v == null ? defaultValue : v.intValue();
+	}
+
+	/**
+	 * Obtain an integer value from the configuration which must be inside given
+	 * range.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param minValue
+	 *            minimum value
+	 * @param maxValue
+	 *            maximum value
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getIntInRange(String section, String subsection, String name,
+			int minValue, int maxValue) {
 		return typedGetter.getIntInRange(this, section, subsection, name,
-				minValue, maxValue, defaultValue);
+				minValue, maxValue, null);
 	}
 
 	/**
@@ -338,7 +423,23 @@ public int getIntInRange(String section, String subsection, String name,
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
 	public long getLong(String section, String name, long defaultValue) {
-		return typedGetter.getLong(this, section, null, name, defaultValue);
+		return getLong(section, null, name, defaultValue);
+	}
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Long getLong(String section, String name) {
+		return getLong(section, null, name);
 	}
 
 	/**
@@ -355,9 +456,28 @@ public long getLong(String section, String name, long defaultValue) {
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
 	public long getLong(final String section, String subsection,
-			final String name, final long defaultValue) {
-		return typedGetter.getLong(this, section, subsection, name,
-				defaultValue);
+			String name, long defaultValue) {
+		Long v = typedGetter.getLong(this, section, subsection, name,
+				Long.valueOf(defaultValue));
+		return v == null ? defaultValue : v.longValue();
+	}
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Long getLong(String section, String subsection, String name) {
+		return typedGetter.getLong(this, section, subsection, name, null);
 	}
 
 	/**
@@ -372,9 +492,26 @@ public long getLong(final String section, String subsection,
 	 * @return true if any value or defaultValue is true, false for missing or
 	 *         explicit false
 	 */
-	public boolean getBoolean(final String section, final String name,
-			final boolean defaultValue) {
-		return typedGetter.getBoolean(this, section, null, name, defaultValue);
+	public boolean getBoolean(String section, String name,
+			boolean defaultValue) {
+		Boolean v = typedGetter.getBoolean(this, section, null, name,
+				Boolean.valueOf(defaultValue));
+		return v == null ? defaultValue : v.booleanValue();
+	}
+
+	/**
+	 * Get a boolean value from the git config
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @return configured boolean value, or {@code null} if not set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Boolean getBoolean(String section, String name) {
+		return getBoolean(section, null, name);
 	}
 
 	/**
@@ -391,10 +528,28 @@ public boolean getBoolean(final String section, final String name,
 	 * @return true if any value or defaultValue is true, false for missing or
 	 *         explicit false
 	 */
-	public boolean getBoolean(final String section, String subsection,
-			final String name, final boolean defaultValue) {
-		return typedGetter.getBoolean(this, section, subsection, name,
-				defaultValue);
+	public boolean getBoolean(String section, String subsection, String name,
+			boolean defaultValue) {
+		Boolean v = typedGetter.getBoolean(this, section, subsection, name,
+				Boolean.valueOf(defaultValue));
+		return v == null ? defaultValue : v.booleanValue();
+	}
+
+	/**
+	 * Get a boolean value from the git config
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return configured boolean value, or {@code null} if not set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Boolean getBoolean(String section, String subsection, String name) {
+		return typedGetter.getBoolean(this, section, subsection, name, null);
 	}
 
 	/**
@@ -412,8 +567,8 @@ public boolean getBoolean(final String section, String subsection,
 	 *            default value to return if no value was present.
 	 * @return the selected enumeration value, or {@code defaultValue}.
 	 */
-	public <T extends Enum<?>> T getEnum(final String section,
-			final String subsection, final String name, final T defaultValue) {
+	public <T extends Enum<?>> T getEnum(String section, String subsection,
+			String name, @NonNull T defaultValue) {
 		final T[] all = allValuesOf(defaultValue);
 		return typedGetter.getEnum(this, all, section, subsection, name,
 				defaultValue);
@@ -448,14 +603,41 @@ public <T extends Enum<?>> T getEnum(final String section,
 	 * @param defaultValue
 	 *            default value to return if no value was present.
 	 * @return the selected enumeration value, or {@code defaultValue}.
+	 * @deprecated use {@link #getEnum(String, String, String, Enum)} or
+	 *             {{@link #getEnum(Enum[], String, String, String)}} instead.
 	 */
-	public <T extends Enum<?>> T getEnum(final T[] all, final String section,
-			final String subsection, final String name, final T defaultValue) {
+	@Nullable
+	@Deprecated
+	public <T extends Enum<?>> T getEnum(T[] all, String section,
+			String subsection, String name, @Nullable T defaultValue) {
 		return typedGetter.getEnum(this, all, section, subsection, name,
 				defaultValue);
 	}
 
 	/**
+	 * Parse an enumeration from the configuration.
+	 *
+	 * @param <T>
+	 *            type of the returned enum
+	 * @param all
+	 *            all possible values in the enumeration which should be
+	 *            recognized. Typically {@code EnumType.values()}.
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return the selected enumeration value, or {@code null} if not set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public <T extends Enum<?>> T getEnum(T[] all, String section,
+			String subsection, String name) {
+		return typedGetter.getEnum(this, all, section, subsection, name, null);
+	}
+
+	/**
 	 * Get string value or null if not found.
 	 *
 	 * @param section
@@ -466,8 +648,8 @@ public <T extends Enum<?>> T getEnum(final T[] all, final String section,
 	 *            the key name
 	 * @return a String value from the config, <code>null</code> if not found
 	 */
-	public String getString(final String section, String subsection,
-			final String name) {
+	@Nullable
+	public String getString(String section, String subsection, String name) {
 		return getRawString(section, subsection, name);
 	}
 
@@ -526,8 +708,34 @@ public String getString(final String section, String subsection,
 	 */
 	public long getTimeUnit(String section, String subsection, String name,
 			long defaultValue, TimeUnit wantUnit) {
+		Long v = typedGetter.getTimeUnit(this, section, subsection, name,
+				Long.valueOf(defaultValue), wantUnit);
+		return v == null ? defaultValue : v.longValue();
+
+	}
+
+	/**
+	 * Parse a numerical time unit, such as "1 minute", from the configuration.
+	 *
+	 * @param section
+	 *            section the key is in.
+	 * @param subsection
+	 *            subsection the key is in, or null if not in a subsection.
+	 * @param name
+	 *            the key name.
+	 * @param wantUnit
+	 *            the units of {@code defaultValue} and the return value, as
+	 *            well as the units to assume if the value does not contain an
+	 *            indication of the units.
+	 * @return the value, or {@code null} if not set, expressed in
+	 *         {@code units}.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Long getTimeUnit(String section, String subsection, String name,
+			TimeUnit wantUnit) {
 		return typedGetter.getTimeUnit(this, section, subsection, name,
-				defaultValue, wantUnit);
+				null, wantUnit);
 	}
 
 	/**
@@ -555,8 +763,9 @@ public long getTimeUnit(String section, String subsection, String name,
 	 * @return the {@link Path}, or {@code defaultValue} if not set
 	 * @since 5.10
 	 */
+	@Nullable
 	public Path getPath(String section, String subsection, String name,
-			@NonNull FS fs, File resolveAgainst, Path defaultValue) {
+			@NonNull FS fs, File resolveAgainst, @Nullable Path defaultValue) {
 		return typedGetter.getPath(this, section, subsection, name, fs,
 				resolveAgainst, defaultValue);
 	}
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 5068a6c..30e7de4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -600,11 +600,21 @@ public final class ConfigConstants {
 
 	/**
 	 * The "trustfolderstat" key in the "core" section
+	 *
 	 * @since 3.6
+	 * @deprecated use {CONFIG_KEY_TRUST_STAT} instead
 	 */
+	@Deprecated(since = "7.2", forRemoval = true)
 	public static final String CONFIG_KEY_TRUSTFOLDERSTAT = "trustfolderstat";
 
 	/**
+	 * The "trustfilestat" key in the "core"section
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_TRUST_STAT = "truststat";
+
+	/**
 	 * The "supportsAtomicFileCreation" key in the "core" section
 	 *
 	 * @since 4.5
@@ -1023,6 +1033,19 @@ public final class ConfigConstants {
 	public static final String CONFIG_KEY_TRUST_LOOSE_REF_STAT = "trustLooseRefStat";
 
 	/**
+	 * The "trustLooseRefStat" key
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_TRUST_PACK_STAT = "trustPackStat";
+
+	/**
+	 * The "trustLooseObjectFileStat" key
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_TRUST_LOOSE_OBJECT_STAT = "trustLooseObjectStat";
+	/**
 	 * The "pack.preserveOldPacks" key
 	 *
 	 * @since 5.13.2
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
index 49602a7..e43c965 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
@@ -17,12 +17,16 @@
 
 import static java.util.zip.Deflater.DEFAULT_COMPRESSION;
 
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Config.SectionParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * This class keeps git repository core parameters.
  */
 public class CoreConfig {
+	private static final Logger LOG = LoggerFactory.getLogger(CoreConfig.class);
 	/** Key for {@link Config#get(SectionParser)}. */
 	public static final Config.SectionParser<CoreConfig> KEY = CoreConfig::new;
 
@@ -127,7 +131,9 @@ public enum LogRefUpdates {
 	 * Permissible values for {@code core.trustPackedRefsStat}.
 	 *
 	 * @since 6.1.1
+	 * @deprecated use {@link TrustStat} instead
 	 */
+	@Deprecated(since = "7.2", forRemoval = true)
 	public enum TrustPackedRefsStat {
 		/** Do not trust file attributes of the packed-refs file. */
 		NEVER,
@@ -135,12 +141,15 @@ public enum TrustPackedRefsStat {
 		/** Trust file attributes of the packed-refs file. */
 		ALWAYS,
 
-		/** Open and close the packed-refs file to refresh its file attributes
-		 * and then trust it. */
+		/**
+		 * Open and close the packed-refs file to refresh its file attributes
+		 * and then trust it.
+		 */
 		AFTER_OPEN,
 
-		/** {@code core.trustPackedRefsStat} defaults to this when it is
-		 * not set */
+		/**
+		 * {@code core.trustPackedRefsStat} defaults to this when it is not set
+		 */
 		UNSET
 	}
 
@@ -148,17 +157,44 @@ public enum TrustPackedRefsStat {
 	 * Permissible values for {@code core.trustLooseRefStat}.
 	 *
 	 * @since 6.9
+	 * @deprecated use {@link TrustStat} instead
 	 */
+	@Deprecated(since = "7.2", forRemoval = true)
 	public enum TrustLooseRefStat {
 
 		/** Trust file attributes of the loose ref. */
 		ALWAYS,
 
-		/** Open and close parent directories of the loose ref file until the
-		 * repository root to refresh its file attributes and then trust it. */
+		/**
+		 * Open and close parent directories of the loose ref file until the
+		 * repository root to refresh its file attributes and then trust it.
+		 */
 		AFTER_OPEN,
 	}
 
+	/**
+	 * Values for {@code core.trustXXX} options.
+	 *
+	 * @since 7.2
+	 */
+	public enum TrustStat {
+		/** Do not trust file attributes of a File. */
+		NEVER,
+
+		/** Always trust file attributes of a File. */
+		ALWAYS,
+
+		/** Open and close the File to refresh its file attributes
+		 * and then trust it. */
+		AFTER_OPEN,
+
+		/**
+		 * Used for specific options to inherit value from value set for
+		 * core.trustStat.
+		 */
+		INHERIT
+	}
+
 	private final int compression;
 
 	private final int packIndexVersion;
@@ -169,6 +205,16 @@ public enum TrustLooseRefStat {
 
 	private final boolean commitGraph;
 
+	private final TrustStat trustStat;
+
+	private final TrustStat trustPackedRefsStat;
+
+	private final TrustStat trustLooseRefStat;
+
+	private final TrustStat trustPackStat;
+
+	private final TrustStat trustLooseObjectStat;
+
 	/**
 	 * Options for symlink handling
 	 *
@@ -198,7 +244,13 @@ public enum HideDotFiles {
 		DOTGITONLY
 	}
 
-	private CoreConfig(Config rc) {
+	/**
+	 * Create a new core configuration from the passed configuration.
+	 *
+	 * @param rc
+	 *            git configuration
+	 */
+	CoreConfig(Config rc) {
 		compression = rc.getInt(ConfigConstants.CONFIG_CORE_SECTION,
 				ConfigConstants.CONFIG_KEY_COMPRESSION, DEFAULT_COMPRESSION);
 		packIndexVersion = rc.getInt(ConfigConstants.CONFIG_PACK_SECTION,
@@ -210,6 +262,60 @@ private CoreConfig(Config rc) {
 		commitGraph = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
 				ConfigConstants.CONFIG_COMMIT_GRAPH,
 				DEFAULT_COMMIT_GRAPH_ENABLE);
+
+		trustStat = parseTrustStat(rc);
+		trustPackedRefsStat = parseTrustPackedRefsStat(rc);
+		trustLooseRefStat = parseTrustLooseRefStat(rc);
+		trustPackStat = parseTrustPackFileStat(rc);
+		trustLooseObjectStat = parseTrustLooseObjectFileStat(rc);
+	}
+
+	private static TrustStat parseTrustStat(Config rc) {
+		Boolean tfs = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
+				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT);
+		TrustStat ts = rc.getEnum(TrustStat.values(),
+				ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_TRUST_STAT);
+		if (tfs != null) {
+			if (ts == null) {
+				LOG.warn(JGitText.get().deprecatedTrustFolderStat);
+				return tfs.booleanValue() ? TrustStat.ALWAYS : TrustStat.NEVER;
+			}
+			LOG.warn(JGitText.get().precedenceTrustConfig);
+		}
+		if (ts == null) {
+			ts = TrustStat.ALWAYS;
+		} else if (ts == TrustStat.INHERIT) {
+			LOG.warn(JGitText.get().invalidTrustStat);
+			ts = TrustStat.ALWAYS;
+		}
+		return ts;
+	}
+
+	private TrustStat parseTrustPackedRefsStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_PACKED_REFS_STAT);
+	}
+
+	private TrustStat parseTrustLooseRefStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_LOOSE_REF_STAT);
+	}
+
+	private TrustStat parseTrustPackFileStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_PACK_STAT);
+	}
+
+	private TrustStat parseTrustLooseObjectFileStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_LOOSE_OBJECT_STAT);
+	}
+
+	private TrustStat inheritParseTrustStat(Config rc, String key) {
+		TrustStat t = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, key,
+				TrustStat.INHERIT);
+		return t == TrustStat.INHERIT ? trustStat : t;
 	}
 
 	/**
@@ -260,4 +366,56 @@ public String getAttributesFile() {
 	public boolean enableCommitGraph() {
 		return commitGraph;
 	}
+
+	/**
+	 * Get how far we can trust file attributes of packed-refs file which is
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of packed-refs file.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustPackedRefsStat() {
+		return trustPackedRefsStat;
+	}
+
+	/**
+	 * Get how far we can trust file attributes of loose ref files which are
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of loose ref files.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustLooseRefStat() {
+		return trustLooseRefStat;
+	}
+
+	/**
+	 * Get how far we can trust file attributes of packed-refs file which is
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of packed-refs file.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustPackStat() {
+		return trustPackStat;
+	}
+
+	/**
+	 * Get how far we can trust file attributes of loose ref files which are
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of loose ref files.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustLooseObjectStat() {
+		return trustLooseObjectStat;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
index a71549c..3059f28 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
@@ -18,6 +18,7 @@
 import java.util.regex.Pattern;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Config.ConfigEnum;
 import org.eclipse.jgit.transport.RefSpec;
@@ -31,27 +32,37 @@
  */
 public class DefaultTypedConfigGetter implements TypedConfigGetter {
 
+	@SuppressWarnings("boxed")
 	@Override
 	public boolean getBoolean(Config config, String section, String subsection,
 			String name, boolean defaultValue) {
+		return neverNull(getBoolean(config, section, subsection, name,
+				Boolean.valueOf(defaultValue)));
+	}
+
+	@Nullable
+	@Override
+	public Boolean getBoolean(Config config, String section, String subsection,
+			String name, @Nullable Boolean defaultValue) {
 		String n = config.getString(section, subsection, name);
 		if (n == null) {
 			return defaultValue;
 		}
 		if (Config.isMissing(n)) {
-			return true;
+			return Boolean.TRUE;
 		}
 		try {
-			return StringUtils.toBoolean(n);
+			return Boolean.valueOf(StringUtils.toBoolean(n));
 		} catch (IllegalArgumentException err) {
 			throw new IllegalArgumentException(MessageFormat.format(
 					JGitText.get().invalidBooleanValue, section, name, n), err);
 		}
 	}
 
+	@Nullable
 	@Override
 	public <T extends Enum<?>> T getEnum(Config config, T[] all, String section,
-			String subsection, String name, T defaultValue) {
+			String subsection, String name, @Nullable T defaultValue) {
 		String value = config.getString(section, subsection, name);
 		if (value == null) {
 			return defaultValue;
@@ -107,9 +118,27 @@ public <T extends Enum<?>> T getEnum(Config config, T[] all, String section,
 	@Override
 	public int getInt(Config config, String section, String subsection,
 			String name, int defaultValue) {
-		long val = config.getLong(section, subsection, name, defaultValue);
+		return neverNull(getInt(config, section, subsection, name,
+				Integer.valueOf(defaultValue)));
+	}
+
+	@Nullable
+	@Override
+	@SuppressWarnings("boxing")
+	public Integer getInt(Config config, String section, String subsection,
+			String name, @Nullable Integer defaultValue) {
+		Long longDefault = defaultValue != null
+				? Long.valueOf(defaultValue.longValue())
+				: null;
+		Long val = config.getLong(section, subsection, name);
+		if (val == null) {
+			val = longDefault;
+		}
+		if (val == null) {
+			return null;
+		}
 		if (Integer.MIN_VALUE <= val && val <= Integer.MAX_VALUE) {
-			return (int) val;
+			return Integer.valueOf(Math.toIntExact(val));
 		}
 		throw new IllegalArgumentException(MessageFormat
 				.format(JGitText.get().integerValueOutOfRange, section, name));
@@ -118,37 +147,56 @@ public int getInt(Config config, String section, String subsection,
 	@Override
 	public int getIntInRange(Config config, String section, String subsection,
 			String name, int minValue, int maxValue, int defaultValue) {
-		int val = getInt(config, section, subsection, name, defaultValue);
+		return neverNull(getIntInRange(config, section, subsection, name,
+				minValue, maxValue, Integer.valueOf(defaultValue)));
+	}
+
+	@Override
+	@SuppressWarnings("boxing")
+	public Integer getIntInRange(Config config, String section,
+			String subsection, String name, int minValue, int maxValue,
+			Integer defaultValue) {
+		Integer val = getInt(config, section, subsection, name, defaultValue);
+		if (val == null) {
+			return null;
+		}
 		if ((val >= minValue && val <= maxValue) || val == UNSET_INT) {
 			return val;
 		}
 		if (subsection == null) {
-			throw new IllegalArgumentException(MessageFormat.format(
-					JGitText.get().integerValueNotInRange, section, name,
-					Integer.valueOf(val), Integer.valueOf(minValue),
-					Integer.valueOf(maxValue)));
+			throw new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().integerValueNotInRange,
+							section, name, val, minValue, maxValue));
 		}
 		throw new IllegalArgumentException(MessageFormat.format(
 				JGitText.get().integerValueNotInRangeSubSection, section,
-				subsection, name, Integer.valueOf(val),
-				Integer.valueOf(minValue), Integer.valueOf(maxValue)));
+				subsection, name, val, minValue, maxValue));
 	}
 
 	@Override
 	public long getLong(Config config, String section, String subsection,
 			String name, long defaultValue) {
-		final String str = config.getString(section, subsection, name);
+		return neverNull(getLong(config, section, subsection, name,
+				Long.valueOf(defaultValue)));
+	}
+
+	@Nullable
+	@Override
+	public Long getLong(Config config, String section, String subsection,
+			String name, @Nullable Long defaultValue) {
+		String str = config.getString(section, subsection, name);
 		if (str == null) {
 			return defaultValue;
 		}
 		try {
-			return StringUtils.parseLongWithSuffix(str, false);
+			return Long.valueOf(StringUtils.parseLongWithSuffix(str, false));
 		} catch (StringIndexOutOfBoundsException e) {
 			// Empty
 			return defaultValue;
 		} catch (NumberFormatException nfe) {
-			throw new IllegalArgumentException(MessageFormat.format(
-					JGitText.get().invalidIntegerValue, section, name, str),
+			throw new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().invalidIntegerValue,
+							section, name, str),
 					nfe);
 		}
 	}
@@ -156,6 +204,13 @@ public long getLong(Config config, String section, String subsection,
 	@Override
 	public long getTimeUnit(Config config, String section, String subsection,
 			String name, long defaultValue, TimeUnit wantUnit) {
+		return neverNull(getTimeUnit(config, section, subsection, name,
+				Long.valueOf(defaultValue), wantUnit));
+	}
+
+	@Override
+	public Long getTimeUnit(Config config, String section, String subsection,
+			String name, @Nullable Long defaultValue, TimeUnit wantUnit) {
 		String valueString = config.getString(section, subsection, name);
 
 		if (valueString == null) {
@@ -232,8 +287,8 @@ public long getTimeUnit(Config config, String section, String subsection,
 		}
 
 		try {
-			return wantUnit.convert(Long.parseLong(digits) * inputMul,
-					inputUnit);
+			return Long.valueOf(wantUnit
+					.convert(Long.parseLong(digits) * inputMul, inputUnit));
 		} catch (NumberFormatException nfe) {
 			IllegalArgumentException iae = notTimeUnit(section, subsection,
 					unitName, valueString);
@@ -274,4 +329,14 @@ public List<RefSpec> getRefSpecs(Config config, String section,
 		}
 		return result;
 	}
+
+	// Trick for the checkers. When we use this, one is never null, but
+	// they don't know.
+	@NonNull
+	private static <T> T neverNull(T one) {
+		if (one == null) {
+			throw new IllegalArgumentException();
+		}
+		return one;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
index 76ed36a..23d16db 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
@@ -74,8 +74,7 @@ public String toConfigValue() {
 	 *            the config to read from
 	 */
 	public GpgConfig(Config config) {
-		keyFormat = config.getEnum(GpgFormat.values(),
-				ConfigConstants.CONFIG_GPG_SECTION, null,
+		keyFormat = config.getEnum(ConfigConstants.CONFIG_GPG_SECTION, null,
 				ConfigConstants.CONFIG_KEY_FORMAT, GpgFormat.OPENPGP);
 		signingKey = config.getString(ConfigConstants.CONFIG_USER_SECTION, null,
 				ConfigConstants.CONFIG_KEY_SIGNINGKEY);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
index 0c03adc..3d4e0d1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
@@ -17,6 +17,7 @@
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.util.FS;
 
@@ -50,11 +51,36 @@ public interface TypedConfigGetter {
 	 *            default value to return if no value was present.
 	 * @return true if any value or defaultValue is true, false for missing or
 	 *         explicit false
+	 * @deprecated use
+	 *             {@link #getBoolean(Config, String, String, String, Boolean)}
+	 *             instead
 	 */
+	@Deprecated
 	boolean getBoolean(Config config, String section, String subsection,
 			String name, boolean defaultValue);
 
 	/**
+	 * Get a boolean value from a git {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @return true if any value or defaultValue is true, false for missing or
+	 *         explicit false
+	 * @since 7.2
+	 */
+	@Nullable
+	Boolean getBoolean(Config config, String section, String subsection,
+			String name, @Nullable Boolean defaultValue);
+
+	/**
 	 * Parse an enumeration from a git {@link Config}.
 	 *
 	 * @param <T>
@@ -74,8 +100,9 @@ boolean getBoolean(Config config, String section, String subsection,
 	 *            default value to return if no value was present.
 	 * @return the selected enumeration value, or {@code defaultValue}.
 	 */
+	@Nullable
 	<T extends Enum<?>> T getEnum(Config config, T[] all, String section,
-			String subsection, String name, T defaultValue);
+			String subsection, String name, @Nullable T defaultValue);
 
 	/**
 	 * Obtain an integer value from a git {@link Config}.
@@ -91,11 +118,34 @@ <T extends Enum<?>> T getEnum(Config config, T[] all, String section,
 	 * @param defaultValue
 	 *            default value to return if no value was present.
 	 * @return an integer value from the configuration, or defaultValue.
+	 * @deprecated use {@link #getInt(Config, String, String, String, Integer)}
+	 *             instead
 	 */
+	@Deprecated
 	int getInt(Config config, String section, String subsection, String name,
 			int defaultValue);
 
 	/**
+	 * Obtain an integer value from a git {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @return an integer value from the configuration, or defaultValue.
+	 * @since 7.2
+	 */
+	@Nullable
+	Integer getInt(Config config, String section, String subsection,
+			String name, @Nullable Integer defaultValue);
+
+	/**
 	 * Obtain an integer value from a git {@link Config} which must be in given
 	 * range.
 	 *
@@ -117,11 +167,43 @@ int getInt(Config config, String section, String subsection, String name,
 	 * @return an integer value from the configuration, or defaultValue.
 	 *         {@code #UNSET_INT} if unset.
 	 * @since 6.1
+	 * @deprecated use
+	 *             {@link #getIntInRange(Config, String, String, String, int, int, Integer)}
+	 *             instead
 	 */
+	@Deprecated
 	int getIntInRange(Config config, String section, String subsection,
 			String name, int minValue, int maxValue, int defaultValue);
 
 	/**
+	 * Obtain an integer value from a git {@link Config} which must be in given
+	 * range.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param minValue
+	 *            minimal value
+	 * @param maxValue
+	 *            maximum value
+	 * @param defaultValue
+	 *            default value to return if no value was present. Use
+	 *            {@code #UNSET_INT} to set the default to unset.
+	 * @return an integer value from the configuration, or defaultValue.
+	 *         {@code #UNSET_INT} if unset.
+	 * @since 7.2
+	 */
+	@Nullable
+	Integer getIntInRange(Config config, String section, String subsection,
+			String name, int minValue, int maxValue,
+			@Nullable Integer defaultValue);
+
+	/**
 	 * Obtain a long value from a git {@link Config}.
 	 *
 	 * @param config
@@ -135,11 +217,34 @@ int getIntInRange(Config config, String section, String subsection,
 	 * @param defaultValue
 	 *            default value to return if no value was present.
 	 * @return a long value from the configuration, or defaultValue.
+	 * @deprecated use {@link #getLong(Config, String, String, String, Long)}
+	 *             instead
 	 */
+	@Deprecated
 	long getLong(Config config, String section, String subsection, String name,
 			long defaultValue);
 
 	/**
+	 * Obtain a long value from a git {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @return a long value from the configuration, or defaultValue.
+	 * @since 7.2
+	 */
+	@Nullable
+	Long getLong(Config config, String section, String subsection, String name,
+			@Nullable Long defaultValue);
+
+	/**
 	 * Parse a numerical time unit, such as "1 minute", from a git
 	 * {@link Config}.
 	 *
@@ -159,11 +264,41 @@ long getLong(Config config, String section, String subsection, String name,
 	 *            indication of the units.
 	 * @return the value, or {@code defaultValue} if not set, expressed in
 	 *         {@code units}.
+	 * @deprecated use
+	 *             {@link #getTimeUnit(Config, String, String, String, Long, TimeUnit)}
+	 *             instead
 	 */
+	@Deprecated
 	long getTimeUnit(Config config, String section, String subsection,
 			String name, long defaultValue, TimeUnit wantUnit);
 
 	/**
+	 * Parse a numerical time unit, such as "1 minute", from a git
+	 * {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is in.
+	 * @param subsection
+	 *            subsection the key is in, or null if not in a subsection.
+	 * @param name
+	 *            the key name.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @param wantUnit
+	 *            the units of {@code defaultValue} and the return value, as
+	 *            well as the units to assume if the value does not contain an
+	 *            indication of the units.
+	 * @return the value, or {@code defaultValue} if not set, expressed in
+	 *         {@code units}.
+	 * @since 7.2
+	 */
+	@Nullable
+	Long getTimeUnit(Config config, String section, String subsection,
+			String name, @Nullable Long defaultValue, TimeUnit wantUnit);
+
+	/**
 	 * Parse a string value from a git {@link Config} and treat it as a file
 	 * path, replacing a ~/ prefix by the user's home directory.
 	 * <p>
@@ -189,9 +324,10 @@ long getTimeUnit(Config config, String section, String subsection,
 	 * @return the {@link Path}, or {@code defaultValue} if not set
 	 * @since 5.10
 	 */
+	@Nullable
 	default Path getPath(Config config, String section, String subsection,
 			String name, @NonNull FS fs, File resolveAgainst,
-			Path defaultValue) {
+			@Nullable Path defaultValue) {
 		String value = config.getString(section, subsection, name);
 		if (value == null) {
 			return defaultValue;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
index becc808..105cba7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
@@ -787,14 +787,14 @@ public IgnoreSubmoduleMode getModulesIgnore() throws IOException,
 		IgnoreSubmoduleMode mode = repoConfig.getEnum(
 				IgnoreSubmoduleMode.values(),
 				ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(),
-				ConfigConstants.CONFIG_KEY_IGNORE, null);
+				ConfigConstants.CONFIG_KEY_IGNORE);
 		if (mode != null) {
 			return mode;
 		}
 		lazyLoadModulesConfig();
-		return modulesConfig.getEnum(IgnoreSubmoduleMode.values(),
-				ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(),
-				ConfigConstants.CONFIG_KEY_IGNORE, IgnoreSubmoduleMode.NONE);
+		return modulesConfig.getEnum(ConfigConstants.CONFIG_SUBMODULE_SECTION,
+				getModuleName(), ConfigConstants.CONFIG_KEY_IGNORE,
+				IgnoreSubmoduleMode.NONE);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
index 73eddb8..f10b7bf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
@@ -302,8 +302,7 @@ private void init(Config config, URIish uri) {
 		int postBufferSize = config.getInt(HTTP, POST_BUFFER_KEY,
 				1 * 1024 * 1024);
 		boolean sslVerifyFlag = config.getBoolean(HTTP, SSL_VERIFY_KEY, true);
-		HttpRedirectMode followRedirectsMode = config.getEnum(
-				HttpRedirectMode.values(), HTTP, null,
+		HttpRedirectMode followRedirectsMode = config.getEnum(HTTP, null,
 				FOLLOW_REDIRECTS_KEY, HttpRedirectMode.INITIAL);
 		int redirectLimit = config.getInt(HTTP, MAX_REDIRECTS_KEY,
 				MAX_REDIRECTS);
@@ -335,8 +334,8 @@ private void init(Config config, URIish uri) {
 					postBufferSize);
 			sslVerifyFlag = config.getBoolean(HTTP, match, SSL_VERIFY_KEY,
 					sslVerifyFlag);
-			followRedirectsMode = config.getEnum(HttpRedirectMode.values(),
-					HTTP, match, FOLLOW_REDIRECTS_KEY, followRedirectsMode);
+			followRedirectsMode = config.getEnum(HTTP, match,
+					FOLLOW_REDIRECTS_KEY, followRedirectsMode);
 			int newMaxRedirects = config.getInt(HTTP, match, MAX_REDIRECTS_KEY,
 					redirectLimit);
 			if (newMaxRedirects >= 0) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
index 4de6ff8..7b5842b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
@@ -82,7 +82,7 @@ public class URIish implements Serializable {
 	 * Part of a pattern which matches a relative path. Relative paths don't
 	 * start with slash or drive letters. Defines no capturing group.
 	 */
-	private static final String RELATIVE_PATH_P = "(?:(?:[^\\\\/]+[\\\\/]+)*[^\\\\/]+[\\\\/]*)"; //$NON-NLS-1$
+	private static final String RELATIVE_PATH_P = "(?:(?:[^\\\\/]+[\\\\/]+)*+[^\\\\/]*)"; //$NON-NLS-1$
 
 	/**
 	 * Part of a pattern which matches a relative or absolute path. Defines no
@@ -120,7 +120,7 @@ public class URIish implements Serializable {
 	 * path (maybe even containing windows drive-letters) or a relative path.
 	 */
 	private static final Pattern LOCAL_FILE = Pattern.compile("^" // //$NON-NLS-1$
-			+ "([\\\\/]?" + PATH_P + ")" // //$NON-NLS-1$ //$NON-NLS-2$
+			+ "([\\\\/]?+" + PATH_P + ")" // //$NON-NLS-1$ //$NON-NLS-2$
 			+ "$"); //$NON-NLS-1$
 
 	/**