diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index 66cf739ef15f3f2cddf080e2faffb714700f547a..c6cdfafe84b386e83f73714714e6becf57ee8704 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -1020,7 +1020,8 @@ public void close() {
 	private static void prunePacked(ObjectDirectory odb) throws IOException {
 		for (Pack p : odb.getPacks()) {
 			for (MutableEntry e : p)
-				FileUtils.delete(odb.fileFor(e.toObjectId()));
+				FileUtils.delete(odb.fileFor(e.toObjectId()),
+						FileUtils.SKIP_MISSING);
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.c b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.c
new file mode 100644
index 0000000000000000000000000000000000000000..366116092188f04fca1e85b50f101b093abf92d8
Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.c differ
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.javasource b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.javasource
new file mode 100644
index 0000000000000000000000000000000000000000..9659685c634d49ae837a96f7eefb789ca34133e5
Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.javasource differ
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.py b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.py
new file mode 100644
index 0000000000000000000000000000000000000000..9eda6cd8fe526d57f50ff6f4b3c9b379afdc5025
Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.py differ
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.rs b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a3aa5cbe7c617c65bc15ba1f8710e0f6ce634c43
Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.rs differ
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/sample.dtsi b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/sample.dtsi
new file mode 100644
index 0000000000000000000000000000000000000000..6aa4ecdd4cb442e52be17a1e93d556acd68af7f8
Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/sample.dtsi differ
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..13528719833f42ce6877f52dacf3a127506ad3cc
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License 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.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.junit.Test;
+
+public class DiffFormatterBuiltInDriverTest extends RepositoryTestCase {
+	@Test
+	public void testCppDriver() throws Exception {
+		String fileName = "greeting.c";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.c   diff=cpp");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings")
+							.replace("baz", "qux"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected =
+					"@@ -27,7 +27,7 @@ void getPersonalizedGreeting(char *result, const char *name, const char *timeOfD\n"
+							+ "@@ -37,7 +37,7 @@ int main() {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testDtsDriver() throws Exception {
+		String fileName = "sample.dtsi";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.dtsi   diff=dts");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("clock-frequency = <24000000>",
+							"clock-frequency = <48000000>"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected = "@@ -20,6 +20,6 @@ uart0: uart@101f1000 {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testJavaDriver() throws Exception {
+		String resourceName = "greeting.javasource";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(resourceName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.java   diff=java");
+			String fileName = "Greeting.java";
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings")
+							.replace("baz", "qux"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected =
+					"@@ -22,7 +22,7 @@ public String getPersonalizedGreeting(String name, String timeOfDay) {\n"
+							+ "@@ -32,6 +32,6 @@ public static void main(String[] args) {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testPythonDriver() throws Exception {
+		String fileName = "greeting.py";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.py   diff=python");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected = "@@ -16,7 +16,7 @@ def get_personalized_greeting(self, name, time_of_day):";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testRustDriver() throws Exception {
+		String fileName = "greeting.rs";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.rs   diff=rust");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings")
+							.replace("baz", "qux"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected =
+					"@@ -14,7 +14,7 @@ fn get_personalized_greeting(&self, name: &str, time_of_day: &str) -> String {\n"
+							+ "@@ -23,5 +23,5 @@ fn main() {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	private String getHunkHeaders(RevCommit c1, RevCommit c2,
+			ByteArrayOutputStream os, DiffFormatter diffFormatter)
+			throws IOException {
+		diffFormatter.setRepository(db);
+		diffFormatter.format(new CanonicalTreeParser(null, db.newObjectReader(),
+						c1.getTree()),
+				new CanonicalTreeParser(null, db.newObjectReader(),
+						c2.getTree()));
+		diffFormatter.flush();
+		return Arrays.stream(os.toString(StandardCharsets.UTF_8).split("\n"))
+				.filter(line -> line.startsWith("@@"))
+				.collect(Collectors.joining("\n"));
+	}
+
+	private RevCommit createCommit(Git git, String fileName, String body)
+			throws IOException, GitAPIException {
+		writeTrashFile(fileName, body);
+		git.add().addFilepattern(".").call();
+		return git.commit().setMessage("message").call();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesAfterBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesAfterBitmapStatisticsTest.java
deleted file mode 100644
index e5a391f2e322de54fbc2ce49c71d03ff029253fb..0000000000000000000000000000000000000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesAfterBitmapStatisticsTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.file;
-
-import static org.junit.Assert.assertEquals;
-
-import java.io.BufferedOutputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.stream.StreamSupport;
-
-import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
-import org.eclipse.jgit.internal.storage.pack.PackExt;
-import org.eclipse.jgit.internal.storage.pack.PackWriter;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.util.FileUtils;
-import org.junit.Test;
-
-public class GcNumberOfPackFilesAfterBitmapStatisticsTest extends GcTestCase {
-	@Test
-	public void testShouldReportZeroObjectsForInitializedRepo()
-			throws IOException {
-		assertEquals(0L, gc.getStatistics().numberOfPackFilesAfterBitmap);
-	}
-
-	@Test
-	public void testShouldReportAllPackFilesWhenNoGcWasPerformed()
-			throws Exception {
-		packAndPrune();
-		long result = gc.getStatistics().numberOfPackFilesAfterBitmap;
-
-		assertEquals(repo.getObjectDatabase().getPacks().size(), result);
-	}
-
-	@Test
-	public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception {
-		// given
-		addCommit(null);
-		gc.gc().get();
-		assertEquals(1L, repositoryBitmapFiles());
-		assertEquals(0L, gc.getStatistics().numberOfPackFilesAfterBitmap);
-	}
-
-	@Test
-	public void testShouldReportNewObjectsAfterGcWhenRepositoryProgresses()
-			throws Exception {
-		// commit & gc
-		RevCommit parent = addCommit(null);
-		gc.gc().get();
-		assertEquals(1L, repositoryBitmapFiles());
-
-		// progress & pack
-		addCommit(parent);
-		packAndPrune();
-
-		assertEquals(1L, gc.getStatistics().numberOfPackFilesAfterBitmap);
-	}
-
-	@Test
-	public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses()
-			throws Exception {
-		// commit & gc
-		RevCommit parent = addCommit(null);
-		gc.gc().get();
-		assertEquals(1L, repositoryBitmapFiles());
-
-		// progress & gc
-		parent = addCommit(parent);
-		gc.gc().get();
-		assertEquals(2L, repositoryBitmapFiles());
-
-		// progress & pack
-		addCommit(parent);
-		packAndPrune();
-
-		assertEquals(1L, gc.getStatistics().numberOfPackFilesAfterBitmap);
-	}
-
-	private void packAndPrune() throws Exception {
-		try (SkipNonExistingFilesTestRepository testRepo = new SkipNonExistingFilesTestRepository(
-				repo)) {
-			testRepo.packAndPrune();
-		}
-	}
-
-	private RevCommit addCommit(RevCommit parent) throws Exception {
-		return tr.branch("master").commit()
-				.author(new PersonIdent("repo-metrics", "repo@metrics.com"))
-				.parent(parent).create();
-	}
-
-	private long repositoryBitmapFiles() throws IOException {
-		return StreamSupport
-				.stream(Files
-						.newDirectoryStream(repo.getObjectDatabase()
-								.getPackDirectory().toPath(), "pack-*.bitmap")
-						.spliterator(), false)
-				.count();
-	}
-
-	/**
-	 * The TestRepository has a {@link TestRepository#packAndPrune()} function
-	 * but it fails in the last step after GC was performed as it doesn't
-	 * SKIP_MISSING files. In order to circumvent it was copied and improved
-	 * here.
-	 */
-	private static class SkipNonExistingFilesTestRepository
-			extends TestRepository<FileRepository> {
-		private final FileRepository repo;
-
-		private SkipNonExistingFilesTestRepository(FileRepository db) throws IOException {
-			super(db);
-			repo = db;
-		}
-
-		@Override
-		public void packAndPrune() throws Exception {
-			ObjectDirectory odb = repo.getObjectDatabase();
-			NullProgressMonitor m = NullProgressMonitor.INSTANCE;
-
-			final PackFile pack, idx;
-			try (PackWriter pw = new PackWriter(repo)) {
-				Set<ObjectId> all = new HashSet<>();
-				for (Ref r : repo.getRefDatabase().getRefs())
-					all.add(r.getObjectId());
-				pw.preparePack(m, all, PackWriter.NONE);
-
-				pack = new PackFile(odb.getPackDirectory(), pw.computeName(),
-						PackExt.PACK);
-				try (OutputStream out = new BufferedOutputStream(
-						new FileOutputStream(pack))) {
-					pw.writePack(m, m, out);
-				}
-				pack.setReadOnly();
-
-				idx = pack.create(PackExt.INDEX);
-				try (OutputStream out = new BufferedOutputStream(
-						new FileOutputStream(idx))) {
-					pw.writeIndex(out);
-				}
-				idx.setReadOnly();
-			}
-
-			odb.openPack(pack);
-			updateServerInfo();
-
-			// alternative packAndPrune implementation that skips missing files
-			// after GC.
-			for (Pack p : odb.getPacks()) {
-				for (MutableEntry e : p)
-					FileUtils.delete(odb.fileFor(e.toObjectId()),
-							FileUtils.SKIP_MISSING);
-			}
-		}
-	}
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cd766c4e91de3e2e3aebf445e91e664b490af16
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.stream.StreamSupport;
+
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class GcSinceBitmapStatisticsTest extends GcTestCase {
+	@Test
+	public void testShouldReportZeroPacksAndObjectsForInitializedRepo()
+			throws IOException {
+		RepoStatistics s = gc.getStatistics();
+		assertEquals(0L, s.numberOfPackFilesSinceBitmap);
+		assertEquals(0L, s.numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportAllPackFilesWhenNoGcWasPerformed()
+			throws Exception {
+		tr.packAndPrune();
+		long result = gc.getStatistics().numberOfPackFilesSinceBitmap;
+
+		assertEquals(repo.getObjectDatabase().getPacks().size(), result);
+	}
+
+	@Test
+	public void testShouldReportAllObjectsWhenNoGcWasPerformed()
+			throws Exception {
+		tr.packAndPrune();
+
+		assertEquals(
+				getNumberOfObjectsInPacks(repo.getObjectDatabase().getPacks()),
+				gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNoPacksDirectlyAfterGc() throws Exception {
+		// given
+		addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+		assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception {
+		// given
+		addCommit(null);
+		assertEquals(2L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		gc.gc().get();
+		assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewPacksSinceGcWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		tr.packAndPrune();
+
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		// progress & pack
+		addCommit(parent);
+		assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		tr.packAndPrune();
+		assertEquals(3L, gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewPacksFromTheLatestBitmapWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & gc
+		parent = addCommit(parent);
+		gc.gc().get();
+		assertEquals(2L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		tr.packAndPrune();
+
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+
+		// progress & gc
+		parent = addCommit(parent);
+		gc.gc().get();
+		assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		// progress & pack
+		addCommit(parent);
+		assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		tr.packAndPrune();
+		assertEquals(4L, gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	private RevCommit addCommit(RevCommit parent) throws Exception {
+		return tr.branch("master").commit()
+				.author(new PersonIdent("repo-metrics", "repo@metrics.com"))
+				.parent(parent).create();
+	}
+
+	private long repositoryBitmapFiles() throws IOException {
+		return StreamSupport
+				.stream(Files
+						.newDirectoryStream(repo.getObjectDatabase()
+								.getPackDirectory().toPath(), "pack-*.bitmap")
+						.spliterator(), false)
+				.count();
+	}
+
+	private long getNumberOfObjectsInPacks(Collection<Pack> packs) {
+		return packs.stream().mapToLong(pack -> {
+			try {
+				return pack.getObjectCount();
+			} catch (IOException e) {
+				throw new RuntimeException(e);
+			}
+		}).sum();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java
new file mode 100644
index 0000000000000000000000000000000000000000..b74444400ed98c23efcc46da8047520bd803d5f1
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License 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.diff;
+
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Built-in drivers for various languages, sorted by name. These drivers will be
+ * used to determine function names for a hunk.
+ * <p>
+ * When writing or updating patterns, assume the contents are syntactically
+ * correct. Patterns can be simple and need not cover all syntactical corner
+ * cases, as long as they are sufficiently permissive.
+ *
+ * @since 6.10.1
+ */
+@SuppressWarnings({"ImmutableEnumChecker", "nls"})
+public enum DiffDriver {
+	/**
+	 * Built-in diff driver for <a href=
+	 * "https://learn.microsoft.com/en-us/cpp/cpp/cpp-language-reference">c++</a>
+	 */
+	cpp(List.of(
+			/* Jump targets or access declarations */
+			"^[ \\t]*[A-Za-z_][A-Za-z_0-9]*:\\s*($|/[/*])"), List.of(
+			/* functions/methods, variables, and compounds at top level */
+			"^((::\\s*)?[A-Za-z_].*)$")),
+	/**
+	 * Built-in diff driver for <a href=
+	 * "https://devicetree-specification.readthedocs.io/en/stable/source-language.html">device
+	 * tree files</a>
+	 */
+	dts(List.of(";", "="), List.of(
+			/* lines beginning with a word optionally preceded by '&' or the root */
+			"^[ \\t]*((/[ \\t]*\\{|&?[a-zA-Z_]).*)")),
+	/**
+	 * Built-in diff driver for <a href=
+	 * "https://docs.oracle.com/javase/specs/jls/se21/html/index.html">java</a>
+	 */
+	java(List.of(
+			"^[ \\t]*(catch|do|for|if|instanceof|new|return|switch|throw|while)"),
+			List.of(
+					/* Class, enum, interface, and record declarations */
+					"^[ \\t]*(([a-z-]+[ \\t]+)*(class|enum|interface|record)[ \\t]+.*)$",
+					/* Method definitions; note that constructor signatures are not */
+					/* matched because they are indistinguishable from method calls. */
+					"^[ \\t]*(([A-Za-z_<>&\\]\\[][?&<>.,A-Za-z_0-9]*[ \\t]+)+[A-Za-z_]"
+							+ "[A-Za-z_0-9]*[ \\t]*\\([^;]*)$")),
+	/**
+	 * Built-in diff driver for
+	 * <a href="https://docs.python.org/3/reference/index.html">python</a>
+	 */
+	python(List.of("^[ \\t]*((class|(async[ \\t]+)?def)[ \\t].*)$")),
+	/**
+	 * Built-in diff driver for
+	 * <a href="https://doc.rust-lang.org/reference/introduction.html">rust</a>
+	 */
+	rust(List.of("^[\\t ]*((pub(\\([^\\)]+\\))?[\\t ]+)?"
+			+ "((async|const|unsafe|extern([\\t ]+\"[^\"]+\"))[\\t ]+)?"
+			+ "(struct|enum|union|mod|trait|fn|impl|macro_rules!)[< \\t]+[^;]*)$"));
+
+	private final List<Pattern> negatePatterns;
+
+	private final List<Pattern> matchPatterns;
+
+	DiffDriver(List<String> negate, List<String> match, int flags) {
+		if (negate != null) {
+			this.negatePatterns = negate.stream()
+					.map(r -> Pattern.compile(r, flags))
+					.collect(Collectors.toList());
+		} else {
+			this.negatePatterns = null;
+		}
+		this.matchPatterns = match.stream().map(r -> Pattern.compile(r, flags))
+				.collect(Collectors.toList());
+	}
+
+	DiffDriver(List<String> match) {
+		this(null, match, 0);
+	}
+
+	DiffDriver(List<String> negate, List<String> match) {
+		this(negate, match, 0);
+	}
+
+	/**
+	 * Returns the list of patterns used to exclude certain lines from being
+	 * considered as function names.
+	 *
+	 * @return the list of negate patterns
+	 */
+	public List<Pattern> getNegatePatterns() {
+		return negatePatterns;
+	}
+
+	/**
+	 * Returns the list of patterns used to match lines for potential function
+	 * names.
+	 *
+	 * @return the list of match patterns
+	 */
+	public List<Pattern> getMatchPatterns() {
+		return matchPatterns;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
index 2f472b5c0aee2c91e9cff8ab83102cfc15eb6aa5..fa446e18cd8eb752d8c5db38982f6d3f3ee10e32 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
@@ -30,7 +30,9 @@
 import java.util.Collections;
 import java.util.List;
 
+import java.util.regex.Pattern;
 import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.attributes.Attribute;
 import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.dircache.DirCacheIterator;
@@ -703,7 +705,7 @@ public void format(List<? extends DiffEntry> entries) throws IOException {
 	 */
 	public void format(DiffEntry ent) throws IOException {
 		FormatResult res = createFormatResult(ent);
-		format(res.header, res.a, res.b);
+		format(res.header, res.a, res.b, getDiffDriver(ent));
 	}
 
 	private static byte[] writeGitLinkText(AbbreviatedObjectId id) {
@@ -749,11 +751,14 @@ private String quotePath(String path) {
 	 *            text source for the post-image version of the content. This
 	 *            must match the content of
 	 *            {@link org.eclipse.jgit.patch.FileHeader#getNewId()}.
+	 * @param diffDriver
+	 *            the diff driver used to obtain function names in hunk headers
 	 * @throws java.io.IOException
-	 *             writing to the supplied stream failed.
+	 *            writing to the supplied stream failed.
+	 * @since 6.10.1
 	 */
-	public void format(FileHeader head, RawText a, RawText b)
-			throws IOException {
+	public void format(FileHeader head, RawText a, RawText b,
+			DiffDriver diffDriver) throws IOException {
 		// Reuse the existing FileHeader as-is by blindly copying its
 		// header lines, but avoiding its hunks. Instead we recreate
 		// the hunks from the text instances we have been supplied.
@@ -763,8 +768,49 @@ public void format(FileHeader head, RawText a, RawText b)
 		if (!head.getHunks().isEmpty())
 			end = head.getHunks().get(0).getStartOffset();
 		out.write(head.getBuffer(), start, end - start);
-		if (head.getPatchType() == PatchType.UNIFIED)
-			format(head.toEditList(), a, b);
+		if (head.getPatchType() == PatchType.UNIFIED) {
+			format(head.toEditList(), a, b, diffDriver);
+		}
+	}
+
+	/**
+	 * Format a patch script, reusing a previously parsed FileHeader.
+	 * <p>
+	 * This formatter is primarily useful for editing an existing patch script
+	 * to increase or reduce the number of lines of context within the script.
+	 * All header lines are reused as-is from the supplied FileHeader.
+	 *
+	 * @param head
+	 * 		existing file header containing the header lines to copy.
+	 * @param a
+	 * 		text source for the pre-image version of the content. This must match
+	 * 		the content of {@link org.eclipse.jgit.patch.FileHeader#getOldId()}.
+	 * @param b
+	 * 		text source for the post-image version of the content. This must match
+	 * 		the content of {@link org.eclipse.jgit.patch.FileHeader#getNewId()}.
+	 * @throws java.io.IOException
+	 * 		writing to the supplied stream failed.
+	 */
+	public void format(FileHeader head, RawText a, RawText b)
+			throws IOException {
+		format(head, a, b, null);
+	}
+
+	/**
+	 * Formats a list of edits in unified diff format
+	 *
+	 * @param edits
+	 * 		some differences which have been calculated between A and B
+	 * @param a
+	 * 		the text A which was compared
+	 * @param b
+	 * 		the text B which was compared
+	 * @throws java.io.IOException
+	 * 		if an IO error occurred
+	 */
+	public void format(EditList edits, RawText a, RawText b)
+			throws IOException {
+		format(edits, a, b, null);
 	}
 
 	/**
@@ -776,11 +822,14 @@ public void format(FileHeader head, RawText a, RawText b)
 	 *            the text A which was compared
 	 * @param b
 	 *            the text B which was compared
+	 * @param diffDriver
+	 *            the diff driver used to obtain function names in hunk headers
 	 * @throws java.io.IOException
 	 *             if an IO error occurred
+	 * @since 6.10.1
 	 */
-	public void format(EditList edits, RawText a, RawText b)
-			throws IOException {
+	public void format(EditList edits, RawText a, RawText b,
+			DiffDriver diffDriver) throws IOException {
 		for (int curIdx = 0; curIdx < edits.size();) {
 			Edit curEdit = edits.get(curIdx);
 			final int endIdx = findCombinedEnd(edits, curIdx);
@@ -791,7 +840,8 @@ public void format(EditList edits, RawText a, RawText b)
 			final int aEnd = (int) Math.min(a.size(), (long) endEdit.getEndA() + context);
 			final int bEnd = (int) Math.min(b.size(), (long) endEdit.getEndB() + context);
 
-			writeHunkHeader(aCur, aEnd, bCur, bEnd);
+			writeHunkHeader(aCur, aEnd, bCur, bEnd,
+					getFuncName(a, aCur - 1, diffDriver));
 
 			while (aCur < aEnd || bCur < bEnd) {
 				if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) {
@@ -881,8 +931,30 @@ protected void writeRemovedLine(RawText text, int line)
 	 * @throws java.io.IOException
 	 *             if an IO error occurred
 	 */
-	protected void writeHunkHeader(int aStartLine, int aEndLine,
-			int bStartLine, int bEndLine) throws IOException {
+	protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine,
+			int bEndLine) throws IOException {
+		writeHunkHeader(aStartLine, aEndLine, bStartLine, bEndLine, null);
+	}
+
+	/**
+	 * Output a hunk header
+	 *
+	 * @param aStartLine
+	 *            within first source
+	 * @param aEndLine
+	 *            within first source
+	 * @param bStartLine
+	 *            within second source
+	 * @param bEndLine
+	 *            within second source
+	 * @param funcName
+	 *            function name of this hunk
+	 * @throws java.io.IOException
+	 *             if an IO error occurred
+	 * @since 6.10.1
+	 */
+	protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine,
+			int bEndLine, String funcName) throws IOException {
 		out.write('@');
 		out.write('@');
 		writeRange('-', aStartLine + 1, aEndLine - aStartLine);
@@ -890,6 +962,10 @@ protected void writeHunkHeader(int aStartLine, int aEndLine,
 		out.write(' ');
 		out.write('@');
 		out.write('@');
+		if (funcName != null) {
+			out.write(' ');
+			out.write(funcName.getBytes());
+		}
 		out.write('\n');
 	}
 
@@ -1247,4 +1323,46 @@ private boolean combineB(List<Edit> e, int i) {
 	private static boolean end(Edit edit, int a, int b) {
 		return edit.getEndA() <= a && edit.getEndB() <= b;
 	}
+
+	private String getFuncName(RawText text, int startAt,
+			DiffDriver diffDriver) {
+		if (diffDriver != null) {
+			while (startAt > 0) {
+				String line = text.getString(startAt);
+				startAt--;
+				if (matchesAny(diffDriver.getNegatePatterns(), line)) {
+					continue;
+				}
+				if (matchesAny(diffDriver.getMatchPatterns(), line)) {
+					String funcName = line.replaceAll("^[ \\t]+", "");
+					return funcName.substring(0,
+							Math.min(funcName.length(), 80)).trim();
+				}
+			}
+		}
+		return null;
+	}
+
+	private boolean matchesAny(List<Pattern> patterns, String text) {
+		if (patterns != null) {
+			for (Pattern p : patterns) {
+				if (p.matcher(text).find()) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	private DiffDriver getDiffDriver(DiffEntry entry) {
+		Attribute diffAttr = entry.getDiffAttribute();
+		if (diffAttr != null) {
+			try {
+				return DiffDriver.valueOf(diffAttr.getValue());
+			} catch (IllegalArgumentException e) {
+				return null;
+			}
+		}
+		return null;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java
index 4343642f9a291282b7a1f559a6ea3d8c35ea7e8a..b401bbe73db80c541335d48c47e9a7fe2c61ba8b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java
@@ -44,8 +44,8 @@ public ObjectId getCalulatedPatchId() {
 	}
 
 	@Override
-	protected void writeHunkHeader(int aStartLine, int aEndLine,
-			int bStartLine, int bEndLine) throws IOException {
+	protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine,
+			int bEndLine, String funcName) throws IOException {
 		// The hunk header is not taken into account for patch id calculation
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index 8fde3903d03f2a66b08b93f1be3a2ff6a6c5242a..f87ca908601aa50fe44e88b5d55551803c776042 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -1512,7 +1512,13 @@ public static class RepoStatistics {
 		 * The number of pack files that were created after the last bitmap
 		 * generation.
 		 */
-		public long numberOfPackFilesAfterBitmap;
+		public long numberOfPackFilesSinceBitmap;
+
+		/**
+		 * The number of objects stored in pack files and as loose object
+		 * created after the last bitmap generation.
+		 */
+		public long numberOfObjectsSinceBitmap;
 
 		/**
 		 * The number of objects stored as loose objects.
@@ -1549,8 +1555,10 @@ public String toString() {
 			final StringBuilder b = new StringBuilder();
 			b.append("numberOfPackedObjects=").append(numberOfPackedObjects); //$NON-NLS-1$
 			b.append(", numberOfPackFiles=").append(numberOfPackFiles); //$NON-NLS-1$
-			b.append(", numberOfPackFilesAfterBitmap=") //$NON-NLS-1$
-					.append(numberOfPackFilesAfterBitmap);
+			b.append(", numberOfPackFilesSinceBitmap=") //$NON-NLS-1$
+					.append(numberOfPackFilesSinceBitmap);
+			b.append(", numberOfObjectsSinceBitmap=") //$NON-NLS-1$
+					.append(numberOfObjectsSinceBitmap);
 			b.append(", numberOfLooseObjects=").append(numberOfLooseObjects); //$NON-NLS-1$
 			b.append(", numberOfLooseRefs=").append(numberOfLooseRefs); //$NON-NLS-1$
 			b.append(", numberOfPackedRefs=").append(numberOfPackedRefs); //$NON-NLS-1$
@@ -1571,14 +1579,19 @@ public String toString() {
 	public RepoStatistics getStatistics() throws IOException {
 		RepoStatistics ret = new RepoStatistics();
 		Collection<Pack> packs = repo.getObjectDatabase().getPacks();
+		long latestBitmapTime = Long.MIN_VALUE;
 		for (Pack p : packs) {
-			ret.numberOfPackedObjects += p.getIndex().getObjectCount();
+			long packedObjects = p.getIndex().getObjectCount();
+			ret.numberOfPackedObjects += packedObjects;
 			ret.numberOfPackFiles++;
 			ret.sizeOfPackedObjects += p.getPackFile().length();
 			if (p.getBitmapIndex() != null) {
 				ret.numberOfBitmaps += p.getBitmapIndex().getBitmapCount();
+				latestBitmapTime = p.getFileSnapshot().lastModifiedInstant()
+						.toEpochMilli();
 			} else {
-				ret.numberOfPackFilesAfterBitmap++;
+				ret.numberOfPackFilesSinceBitmap++;
+				ret.numberOfObjectsSinceBitmap += packedObjects;
 			}
 		}
 		File objDir = repo.getObjectsDirectory();
@@ -1595,6 +1608,9 @@ public RepoStatistics getStatistics() throws IOException {
 						continue;
 					ret.numberOfLooseObjects++;
 					ret.sizeOfLooseObjects += f.length();
+					if (f.lastModified() > latestBitmapTime) {
+						ret.numberOfObjectsSinceBitmap ++;
+					}
 				}
 			}
 		}