Origin: https://build.opensuse.org/request/show/1116733

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 0232156..a959626 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
@@ -275,6 +275,25 @@ public DirCacheEntry file(String path, RevBlob blob)
 		return e;
 	}
 
+	/**
+	 * Construct a symlink mode tree entry.
+	 *
+	 * @param path
+	 *            path of the symlink.
+	 * @param blob
+	 *            a blob, previously constructed in the repository.
+	 * @return the entry.
+	 * @throws Exception
+	 *             if an error occurred
+	 * @since 5.13.3
+	 */
+	public DirCacheEntry link(String path, RevBlob blob) throws Exception {
+		DirCacheEntry e = new DirCacheEntry(path);
+		e.setFileMode(FileMode.SYMLINK);
+		e.setObjectId(blob);
+		return e;
+	}
+
 	/**
 	 * Construct a tree from a specific listing of file entries.
 	 *
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch
new file mode 100644
index 0000000..802fa15
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch
@@ -0,0 +1,9 @@
+diff --git a/.git/b b/.git/b
+new file mode 100644
+index 0000000..de98044
+--- /dev/null
++++ b/.git/b
+@@ -0,0 +1,3 @@
++a
++b
++c
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch
new file mode 100644
index 0000000..03cacba
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch
@@ -0,0 +1,9 @@
+diff --git a/.GIT/b b/.GIT/b
+new file mode 100644
+index 0000000..de98044
+--- /dev/null
++++ b/.git/b
+@@ -0,0 +1,3 @@
++a
++b
++c
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes
new file mode 100644
index 0000000..b38f87f
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes
@@ -0,0 +1 @@
+*.patch -crlf
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch
new file mode 100644
index 0000000..a275c85
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch
@@ -0,0 +1,9 @@
+diff --git a/a/b b/a/b
+new file mode 100644
+index 0000000..de98044
+--- /dev/null
++++ b/a/b
+@@ -0,0 +1,3 @@
++a
++b
++c
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
index 8e84dfa..c3e0f94 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
@@ -46,6 +46,16 @@ public void testIsValidPath() {
 		assertFalse(isValidPath("a/"));
 		assertFalse(isValidPath("ab/cd/ef/"));
 		assertFalse(isValidPath("a\u0000b"));
+		assertFalse(isValidPath(".git"));
+		assertFalse(isValidPath(".GIT"));
+		assertFalse(isValidPath(".Git"));
+		assertFalse(isValidPath(".git/b"));
+		assertFalse(isValidPath(".GIT/b"));
+		assertFalse(isValidPath(".Git/b"));
+		assertFalse(isValidPath("x/y/.git/z/b"));
+		assertFalse(isValidPath("x/y/.GIT/z/b"));
+		assertFalse(isValidPath("x/y/.Git/z/b"));
+		assertTrue(isValidPath("git/b"));
 	}
 
 	@SuppressWarnings("unused")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java
new file mode 100644
index 0000000..e3bc85a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.dircache;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+/**
+ * Tests for checking out with invalid paths.
+ */
+public class InvalidPathCheckoutTest extends RepositoryTestCase {
+
+	private DirCacheEntry brokenEntry(String fileName, RevBlob blob) {
+		DirCacheEntry entry = new DirCacheEntry("XXXX/" + fileName);
+		entry.path[0] = '.';
+		entry.path[1] = 'g';
+		entry.path[2] = 'i';
+		entry.path[3] = 't';
+		entry.setFileMode(FileMode.REGULAR_FILE);
+		entry.setObjectId(blob);
+		return entry;
+	}
+
+	@Test
+	public void testCheckoutIntoDotGit() throws Exception {
+		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+			db.incrementOpen();
+			// DirCacheEntry does not allow any path component to contain
+			// ".git". C git also forbids this. But what if somebody creates
+			// such an entry explicitly?
+			RevCommit base = repo
+					.commit(repo.tree(brokenEntry("b", repo.blob("test"))));
+			try (Git git = new Git(db)) {
+				assertThrows(InvalidPathException.class, () -> git.reset()
+						.setMode(ResetType.HARD).setRef(base.name()).call());
+				File b = new File(new File(trash, ".git"), "b");
+				assertFalse(".git/b should not exist", b.exists());
+			}
+		}
+	}
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java
new file mode 100644
index 0000000..05d4cea
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2023 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.symlinks;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+
+import org.eclipse.jgit.api.ApplyResult;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.api.errors.PatchApplyException;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DirectoryTest extends RepositoryTestCase {
+
+	@BeforeClass
+	public static void checkPrecondition() throws Exception {
+		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+		Path tempDir = Files.createTempDirectory("jgit");
+		try {
+			Path a = tempDir.resolve("a");
+			Files.write(a, "test".getBytes(StandardCharsets.UTF_8));
+			Path b = tempDir.resolve("A");
+			Assume.assumeTrue(Files.exists(b));
+		} finally {
+			FileUtils.delete(tempDir.toFile(),
+					FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
+		}
+	}
+
+	@Parameters(name = "core.symlinks={0}")
+	public static Boolean[] parameters() {
+		return new Boolean[] { Boolean.TRUE, Boolean.FALSE };
+	}
+
+	@Parameter(0)
+	public boolean useSymlinks;
+
+	private void checkFiles() throws Exception {
+		File a = new File(trash, "a");
+		assertTrue("a should be a directory",
+				Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS));
+		File b = new File(a, "b");
+		assertTrue("a/b should exist", b.isFile());
+		File x = new File(trash, "x");
+		assertTrue("x should be a directory",
+				Files.isDirectory(x.toPath(), LinkOption.NOFOLLOW_LINKS));
+		File y = new File(x, "y");
+		assertTrue("x/y should exist", y.isFile());
+	}
+
+	@Test
+	public void testCheckout() throws Exception {
+		StoredConfig config = db.getConfig();
+		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+		config.save();
+		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+			db.incrementOpen();
+			// Create links directly in the git repo, then use a hard reset
+			// to get them into the workspace.
+			RevCommit base = repo.commit(
+					repo.tree(
+							repo.link("A", repo.blob(".git")),
+							repo.file("a/b", repo.blob("test")),
+							repo.file("x/y", repo.blob("test2"))));
+			try (Git git = new Git(db)) {
+				git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+				File b = new File(new File(trash, ".git"), "b");
+				assertFalse(".git/b should not exist", b.exists());
+				checkFiles();
+			}
+		}
+	}
+
+	@Test
+	public void testCheckout2() throws Exception {
+		StoredConfig config = db.getConfig();
+		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+		config.save();
+		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+			db.incrementOpen();
+			RevCommit base = repo.commit(
+					repo.tree(
+							repo.link("A/B", repo.blob("../.git")),
+							repo.file("a/b/a/b", repo.blob("test")),
+							repo.file("x/y", repo.blob("test2"))));
+			try (Git git = new Git(db)) {
+				boolean testFiles = true;
+				try {
+					git.reset().setMode(ResetType.HARD).setRef(base.name())
+							.call();
+				} catch (Exception e) {
+					if (!useSymlinks) {
+						// There is a file in the middle of the path where we'd
+						// expect a directory. This case is not handled
+						// anywhere. What would be a better reply than an IOE?
+						testFiles = false;
+					} else {
+						throw e;
+					}
+				}
+				File a = new File(new File(trash, ".git"), "a");
+				assertFalse(".git/a should not exist", a.exists());
+				if (testFiles) {
+					a = new File(trash, "a");
+					assertTrue("a should be a directory", Files.isDirectory(
+							a.toPath(), LinkOption.NOFOLLOW_LINKS));
+					File b = new File(a, "b");
+					assertTrue("a/b should be a directory", Files.isDirectory(
+							a.toPath(), LinkOption.NOFOLLOW_LINKS));
+					a = new File(b, "a");
+					assertTrue("a/b/a should be a directory", Files.isDirectory(
+							a.toPath(), LinkOption.NOFOLLOW_LINKS));
+					b = new File(a, "b");
+					assertTrue("a/b/a/b should exist", b.isFile());
+					File x = new File(trash, "x");
+					assertTrue("x should be a directory", Files.isDirectory(
+							x.toPath(), LinkOption.NOFOLLOW_LINKS));
+					File y = new File(x, "y");
+					assertTrue("x/y should exist", y.isFile());
+				}
+			}
+		}
+	}
+
+	@Test
+	public void testMerge() throws Exception {
+		StoredConfig config = db.getConfig();
+		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+		config.save();
+		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+			db.incrementOpen();
+			RevCommit base = repo.commit(
+					repo.tree(repo.file("q", repo.blob("test"))));
+			RevCommit side = repo.commit(
+					repo.tree(
+							repo.link("A", repo.blob(".git")),
+							repo.file("a/b", repo.blob("test")),
+							repo.file("x/y", repo.blob("test2"))));
+			try (Git git = new Git(db)) {
+				git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+				git.merge().include(side)
+						.setMessage("merged").call();
+				File b = new File(new File(trash, ".git"), "b");
+				assertFalse(".git/b should not exist", b.exists());
+				checkFiles();
+			}
+		}
+	}
+
+	@Test
+	public void testMerge2() throws Exception {
+		StoredConfig config = db.getConfig();
+		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+		config.save();
+		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+			db.incrementOpen();
+			RevCommit base = repo.commit(
+					repo.tree(
+							repo.file("q", repo.blob("test")),
+							repo.link("A", repo.blob(".git"))));
+			RevCommit side = repo.commit(
+					repo.tree(
+							repo.file("a/b", repo.blob("test")),
+							repo.file("x/y", repo.blob("test2"))));
+			try (Git git = new Git(db)) {
+				git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+				git.merge().include(side)
+						.setMessage("merged").call();
+				File b = new File(new File(trash, ".git"), "b");
+				assertFalse(".git/b should not exist", b.exists());
+				checkFiles();
+			}
+		}
+	}
+
+	@Test
+	public void testApply() throws Exception {
+		StoredConfig config = db.getConfig();
+		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+		config.save();
+		// PatchApplier doesn't do symlinks yet.
+		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+			db.incrementOpen();
+			RevCommit base = repo.commit(
+					repo.tree(
+							repo.file("x", repo.blob("test")),
+							repo.link("A", repo.blob(".git"))));
+			try (Git git = new Git(db)) {
+				boolean testFiles = true;
+				git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+				try (InputStream patchStream = this.getClass()
+						.getResourceAsStream("dirtest.patch")) {
+					ApplyResult result = git.apply().setPatch(patchStream).call();
+					assertNotNull(result);
+				} catch (PatchApplyException e) {
+					if (!useSymlinks) {
+						// There is a file there, so the patch won't apply.
+						// Unclear whether an IOE is the correct response,
+						// though. Probably some negative PatchApplier.Result is
+						// more appropriate.
+						testFiles = false;
+					} else {
+						throw e;
+					}
+				}
+				File b = new File(new File(trash, ".git"), "b");
+				assertFalse(".git/b should not exist", b.exists());
+				if (testFiles) {
+					File a = new File(trash, "a");
+					assertTrue("a should be a directory",
+							Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS));
+					b = new File(a, "b");
+					assertTrue("a/b should exist", b.isFile());
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 83a8381..e19a32f 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,5 +1,21 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.jgit" version="2">
+    <resource path="src/org/eclipse/jgit/dircache/Checkout.java" type="org.eclipse.jgit.dircache.Checkout">
+        <filter id="1109393411">
+            <message_arguments>
+                <message_argument value="5.11.0"/>
+                <message_argument value="org.eclipse.jgit.dircache.Checkout"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/lib/FileModeCache.java" type="org.eclipse.jgit.lib.FileModeCache">
+        <filter id="1109393411">
+            <message_arguments>
+                <message_argument value="5.11.0"/>
+                <message_argument value="org.eclipse.jgit.lib.FileModeCache"/>
+            </message_arguments>
+        </filter>
+    </resource>
     <resource path="src/org/eclipse/jgit/storage/pack/PackConfig.java" type="org.eclipse.jgit.storage.pack.PackConfig">
         <filter id="336658481">
             <message_arguments>
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 3acceab..616ebbd 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -13,6 +13,8 @@ ambiguousObjectAbbreviation=Object abbreviation {0} is ambiguous
 aNewObjectIdIsRequired=A NewObjectId is required.
 anExceptionOccurredWhileTryingToAddTheIdOfHEAD=An exception occurred while trying to add the Id of HEAD
 anSSHSessionHasBeenAlreadyCreated=An SSH session has been already created
+applyPatchDestInvalid=Destination path in patch is invalid
+applyPatchSourceInvalid==Source path in patch is invalid
 applyBinaryBaseOidWrong=Cannot apply binary patch; OID for file {0} does not match
 applyBinaryOidTooShort=Binary patch for file {0} does not have full IDs
 applyBinaryResultOidWrong=Result of binary patch for file {0} has wrong OID.
@@ -543,6 +545,8 @@ packWasDeleted=Pack file {0} was deleted, removing it from pack list
 packWriterStatistics=Total {0,number,#0} (delta {1,number,#0}), reused {2,number,#0} (delta {3,number,#0})
 panicCantRenameIndexFile=Panic: index file {0} must be renamed to replace {1}; until then repository is corrupt
 patchApplyException=Cannot apply: {0}
+patchApplyErrorWithHunk=Error applying patch in {0}, hunk {1}: {2}
+patchApplyErrorWithoutHunk=Error applying patch in {0}: {1}
 patchFormatException=Format error: {0}
 pathNotConfigured=Submodule path is not configured
 peeledLineBeforeRef=Peeled line before ref.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
index 583767a..7e66471 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
@@ -22,10 +22,12 @@
 import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.zip.InflaterInputStream;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.FilterFailedException;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.PatchApplyException;
@@ -34,6 +36,7 @@
 import org.eclipse.jgit.attributes.FilterCommandRegistry;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
@@ -44,6 +47,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.FileModeCache;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectStream;
@@ -64,6 +68,7 @@
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.SystemReader;
 import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
@@ -72,6 +77,12 @@
 import org.eclipse.jgit.util.io.EolStreamTypeUtil;
 import org.eclipse.jgit.util.sha1.SHA1;
 
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
+
 /**
  * Apply a patch to files and/or to the index.
  *
@@ -117,6 +128,7 @@ public ApplyCommand setPatch(InputStream in) {
 	@Override
 	public ApplyResult call() throws GitAPIException, PatchFormatException,
 			PatchApplyException {
+		Result result = new Result();
 		checkCallable();
 		setCallable(false);
 		ApplyResult r = new ApplyResult();
@@ -132,27 +144,31 @@ public ApplyResult call() throws GitAPIException, PatchFormatException,
 			}
 			Repository repository = getRepository();
 			DirCache cache = repository.readDirCache();
+			FileModeCache directoryCache = new FileModeCache(repo);
 			for (FileHeader fh : p.getFiles()) {
 				ChangeType type = fh.getChangeType();
 				File f = null;
+				if (!verifyExistence(fh, new File(repo.getWorkTree(), fh.getOldPath()), new File(repo.getWorkTree(), fh.getNewPath()), result)) {
+					continue;
+				}
 				switch (type) {
 				case ADD:
-					f = getFile(fh.getNewPath(), true);
+					f = getFile(fh.getNewPath(), true, directoryCache);
 					apply(repository, fh.getNewPath(), cache, f, fh);
 					break;
 				case MODIFY:
-					f = getFile(fh.getOldPath(), false);
+					f = getFile(fh.getOldPath(), false, directoryCache);
 					apply(repository, fh.getOldPath(), cache, f, fh);
 					break;
 				case DELETE:
-					f = getFile(fh.getOldPath(), false);
+					f = getFile(fh.getOldPath(), false, directoryCache);
 					if (!f.delete())
 						throw new PatchApplyException(MessageFormat.format(
 								JGitText.get().cannotDeleteFile, f));
 					break;
 				case RENAME:
-					f = getFile(fh.getOldPath(), false);
-					File dest = getFile(fh.getNewPath(), false);
+					f = getFile(fh.getOldPath(), false, directoryCache);
+					File dest = getFile(fh.getNewPath(), false, directoryCache);
 					try {
 						FileUtils.mkdirs(dest.getParentFile(), true);
 						FileUtils.rename(f, dest,
@@ -164,9 +180,9 @@ public ApplyResult call() throws GitAPIException, PatchFormatException,
 					apply(repository, fh.getOldPath(), cache, dest, fh);
 					break;
 				case COPY:
-					f = getFile(fh.getOldPath(), false);
-					File target = getFile(fh.getNewPath(), false);
-					FileUtils.mkdirs(target.getParentFile(), true);
+					f = getFile(fh.getOldPath(), false, directoryCache);
+					File target = getFile(fh.getNewPath(), false, directoryCache);
+					directoryCache.safeCreateParentDirectory(fh.getNewPath(), target.getParentFile(), false);
 					Files.copy(f.toPath(), target.toPath());
 					apply(repository, fh.getOldPath(), cache, target, fh);
 				}
@@ -179,13 +195,122 @@ public ApplyResult call() throws GitAPIException, PatchFormatException,
 		return r;
 	}
 
-	private File getFile(String path, boolean create)
+	/**
+	 * A wrapper for returning both the applied tree ID and the applied files
+	 * list, as well as file specific errors.
+	 *
+	 * @since 6.3
+	 */
+	public static class Result {
+
+		/**
+		 * A wrapper for a patch applying error that affects a given file.
+		 *
+		 * @since 6.6
+		 */
+		// TODO(ms): rename this class in next major release
+		@SuppressWarnings("JavaLangClash")
+		public static class Error {
+
+			private String msg;
+			private String oldFileName;
+			private @Nullable HunkHeader hh;
+
+			private Error(String msg, String oldFileName,
+						  @Nullable HunkHeader hh) {
+				this.msg = msg;
+				this.oldFileName = oldFileName;
+				this.hh = hh;
+			}
+
+			@Override
+			public String toString() {
+				if (hh != null) {
+					return MessageFormat.format(JGitText.get().patchApplyErrorWithHunk,
+							oldFileName, hh, msg);
+				}
+				return MessageFormat.format(JGitText.get().patchApplyErrorWithoutHunk,
+						oldFileName, msg);
+			}
+
+		}
+
+		private ObjectId treeId;
+
+		private List<String> paths;
+
+		private List<Error> errors = new ArrayList<>();
+
+		/**
+		 * Get modified paths
+		 *
+		 * @return List of modified paths.
+		 */
+		public List<String> getPaths() {
+			return paths;
+		}
+
+		/**
+		 * Get tree ID
+		 *
+		 * @return The applied tree ID.
+		 */
+		public ObjectId getTreeId() {
+			return treeId;
+		}
+
+		/**
+		 * Get errors
+		 *
+		 * @return Errors occurred while applying the patch.
+		 *
+		 * @since 6.6
+		 */
+		public List<Error> getErrors() {
+			return errors;
+		}
+
+		private void addError(String msg,String oldFileName, @Nullable HunkHeader hh) {
+			errors.add(new Error(msg, oldFileName, hh));
+		}
+	}
+
+	private boolean verifyExistence(FileHeader fh, File src, File dest,
+									Result result) throws IOException {
+		boolean isValid = true;
+		boolean srcShouldExist = Arrays.asList(new ChangeType[]{MODIFY, DELETE, RENAME, COPY})
+				.contains(fh.getChangeType());
+		boolean destShouldNotExist = Arrays.asList(new ChangeType[]{ADD, RENAME, COPY})
+				.contains(fh.getChangeType());
+		if (srcShouldExist && !validGitPath(fh.getOldPath())) {
+			result.addError(JGitText.get().applyPatchSourceInvalid,
+					fh.getOldPath(), null);
+			isValid = false;
+		}
+		if (destShouldNotExist && !validGitPath(fh.getNewPath())) {
+			result.addError(JGitText.get().applyPatchDestInvalid,
+					fh.getNewPath(), null);
+			isValid = false;
+		}
+		return isValid;
+	}
+
+	private boolean validGitPath(String path) {
+		try {
+			SystemReader.getInstance().checkPath(path);
+			return true;
+		} catch (CorruptObjectException e) {
+			return false;
+		}
+	}
+
+	private File getFile(String path, boolean create, FileModeCache directoryCache)
 			throws PatchApplyException {
 		File f = new File(getRepository().getWorkTree(), path);
 		if (create) {
 			try {
 				File parent = f.getParentFile();
-				FileUtils.mkdirs(parent, true);
+				directoryCache.safeCreateParentDirectory(path, parent, false);
 				FileUtils.createNewFile(f);
 			} catch (IOException e) {
 				throw new PatchApplyException(MessageFormat.format(
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
index 847ab0a..c3f7142 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
@@ -28,6 +28,7 @@
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
 import org.eclipse.jgit.api.errors.RefNotFoundException;
+import org.eclipse.jgit.dircache.Checkout;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
@@ -411,6 +412,7 @@ public CheckoutCommand setAllPaths(boolean all) {
 	protected CheckoutCommand checkoutPaths() throws IOException,
 			RefNotFoundException {
 		actuallyModifiedPaths = new HashSet<>();
+		Checkout checkout = new Checkout(repo).setRecursiveDeletion(true);
 		DirCache dc = repo.lockDirCache();
 		try (RevWalk revWalk = new RevWalk(repo);
 				TreeWalk treeWalk = new TreeWalk(repo,
@@ -419,10 +421,10 @@ protected CheckoutCommand checkoutPaths() throws IOException,
 			if (!checkoutAllPaths)
 				treeWalk.setFilter(PathFilterGroup.createFromStrings(paths));
 			if (isCheckoutIndex())
-				checkoutPathsFromIndex(treeWalk, dc);
+				checkoutPathsFromIndex(treeWalk, dc, checkout);
 			else {
 				RevCommit commit = revWalk.parseCommit(getStartPointObjectId());
-				checkoutPathsFromCommit(treeWalk, dc, commit);
+				checkoutPathsFromCommit(treeWalk, dc, commit, checkout);
 			}
 		} finally {
 			try {
@@ -439,7 +441,8 @@ protected CheckoutCommand checkoutPaths() throws IOException,
 		return this;
 	}
 
-	private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc)
+	private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc,
+			Checkout checkout)
 			throws IOException {
 		DirCacheIterator dci = new DirCacheIterator(dc);
 		treeWalk.addTree(dci);
@@ -465,8 +468,9 @@ public void apply(DirCacheEntry ent) {
 					if (stage > DirCacheEntry.STAGE_0) {
 						if (checkoutStage != null) {
 							if (stage == checkoutStage.number) {
-								checkoutPath(ent, r, new CheckoutMetadata(
-										eolStreamType, filterCommand));
+								checkoutPath(ent, r, checkout, path,
+										new CheckoutMetadata(eolStreamType,
+												filterCommand));
 								actuallyModifiedPaths.add(path);
 							}
 						} else {
@@ -475,7 +479,8 @@ public void apply(DirCacheEntry ent) {
 							throw new JGitInternalException(e.getMessage(), e);
 						}
 					} else {
-						checkoutPath(ent, r, new CheckoutMetadata(eolStreamType,
+						checkoutPath(ent, r, checkout, path,
+								new CheckoutMetadata(eolStreamType,
 								filterCommand));
 						actuallyModifiedPaths.add(path);
 					}
@@ -488,7 +493,7 @@ public void apply(DirCacheEntry ent) {
 	}
 
 	private void checkoutPathsFromCommit(TreeWalk treeWalk, DirCache dc,
-			RevCommit commit) throws IOException {
+			RevCommit commit, Checkout checkout) throws IOException {
 		treeWalk.addTree(commit.getTree());
 		final ObjectReader r = treeWalk.getObjectReader();
 		DirCacheEditor editor = dc.editor();
@@ -510,7 +515,7 @@ public void apply(DirCacheEntry ent) {
 					}
 					ent.setObjectId(blobId);
 					ent.setFileMode(mode);
-					checkoutPath(ent, r,
+					checkoutPath(ent, r, checkout, path,
 							new CheckoutMetadata(eolStreamType, filterCommand));
 					actuallyModifiedPaths.add(path);
 				}
@@ -520,10 +525,9 @@ public void apply(DirCacheEntry ent) {
 	}
 
 	private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
-			CheckoutMetadata checkoutMetadata) {
+			Checkout checkout, String path, CheckoutMetadata checkoutMetadata) {
 		try {
-			DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
-					checkoutMetadata);
+			checkout.checkout(entry, checkoutMetadata, reader, path);
 		} catch (IOException e) {
 			throw new JGitInternalException(MessageFormat.format(
 					JGitText.get().checkoutConflictWithFile,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 1004d3e..aa7cdb2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -23,6 +23,7 @@
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.api.errors.StashApplyFailureException;
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
+import org.eclipse.jgit.dircache.Checkout;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheCheckout;
@@ -382,6 +383,7 @@ private void resetIndex(RevTree tree) throws IOException {
 	private void resetUntracked(RevTree tree) throws CheckoutConflictException,
 			IOException {
 		Set<String> actuallyModifiedPaths = new HashSet<>();
+		Checkout checkout = new Checkout(repo).setRecursiveDeletion(true);
 		// TODO maybe NameConflictTreeWalk ?
 		try (TreeWalk walk = new TreeWalk(repo)) {
 			walk.addTree(tree);
@@ -405,17 +407,17 @@ private void resetUntracked(RevTree tree) throws CheckoutConflictException,
 
 				FileTreeIterator fIter = walk
 						.getTree(1, FileTreeIterator.class);
+				String gitPath = entry.getPathString();
 				if (fIter != null) {
 					if (fIter.isModified(entry, true, reader)) {
 						// file exists and is dirty
-						throw new CheckoutConflictException(
-								entry.getPathString());
+						throw new CheckoutConflictException(gitPath);
 					}
 				}
 
-				checkoutPath(entry, reader,
+				checkoutPath(entry, gitPath, reader, checkout,
 						new CheckoutMetadata(eolStreamType, null));
-				actuallyModifiedPaths.add(entry.getPathString());
+				actuallyModifiedPaths.add(gitPath);
 			}
 		} finally {
 			if (!actuallyModifiedPaths.isEmpty()) {
@@ -425,11 +427,11 @@ private void resetUntracked(RevTree tree) throws CheckoutConflictException,
 		}
 	}
 
-	private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
-			CheckoutMetadata checkoutMetadata) {
+	private void checkoutPath(DirCacheEntry entry, String gitPath,
+			ObjectReader reader,
+			Checkout checkout, CheckoutMetadata checkoutMetadata) {
 		try {
-			DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
-					checkoutMetadata);
+			checkout.checkout(entry, checkoutMetadata, reader, gitPath);
 		} catch (IOException e) {
 			throw new JGitInternalException(MessageFormat.format(
 					JGitText.get().checkoutConflictWithFile,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
new file mode 100644
index 0000000..accf732
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2023, 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.dircache;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.StandardCopyOption;
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.FileModeCache;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
+import org.eclipse.jgit.lib.CoreConfig.SymLinks;
+import org.eclipse.jgit.lib.FileModeCache.CacheItem;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * An object that can be used to check out many files.
+ *
+ * @since 6.6.1
+ */
+public class Checkout {
+
+	private final FileModeCache cache;
+
+	private final WorkingTreeOptions options;
+
+	private boolean recursiveDelete;
+
+	/**
+	 * Creates a new {@link Checkout} for checking out from the given
+	 * repository.
+	 *
+	 * @param repo
+	 *            the {@link Repository} to check out from
+	 */
+	public Checkout(@NonNull Repository repo) {
+		this(repo, null);
+	}
+
+	/**
+	 * Creates a new {@link Checkout} for checking out from the given
+	 * repository.
+	 *
+	 * @param repo
+	 *            the {@link Repository} to check out from
+	 * @param options
+	 *            the {@link WorkingTreeOptions} to use; if {@code null},
+	 *            read from the {@code repo} config when this object is
+	 *            created
+	 */
+	public Checkout(@NonNull Repository repo, WorkingTreeOptions options) {
+		this.cache = new FileModeCache(repo);
+		this.options = options != null ? options
+				: repo.getConfig().get(WorkingTreeOptions.KEY);
+	}
+
+	/**
+	 * Retrieves the {@link WorkingTreeOptions} of the repository that are
+	 * used.
+	 *
+	 * @return the {@link WorkingTreeOptions}
+	 */
+	public WorkingTreeOptions getWorkingTreeOptions() {
+		return options;
+	}
+
+	/**
+	 * Defines whether directories that are in the way of the file to be checked
+	 * out shall be deleted recursively.
+	 *
+	 * @param recursive
+	 *            whether to delete such directories recursively
+	 * @return {@code this}
+	 */
+	public Checkout setRecursiveDeletion(boolean recursive) {
+		this.recursiveDelete = recursive;
+		return this;
+	}
+
+	/**
+	 * Ensure that the given parent directory exists, and cache the information
+	 * that gitPath refers to a file.
+	 *
+	 * @param gitPath
+	 *            of the file to be written
+	 * @param parentDir
+	 *            directory in which the file shall be placed, assumed to be the
+	 *            parent of the {@code gitPath}
+	 * @param makeSpace
+	 *            whether to delete a possibly existing file at
+	 *            {@code parentDir}
+	 * @throws IOException
+	 *             if the directory cannot be created, if necessary
+	 */
+	public void safeCreateParentDirectory(String gitPath, File parentDir,
+			boolean makeSpace) throws IOException {
+		cache.safeCreateParentDirectory(gitPath, parentDir, makeSpace);
+	}
+
+	/**
+	 * Checks out the gitlink given by the {@link DirCacheEntry}.
+	 *
+	 * @param entry
+	 *            {@link DirCacheEntry} to check out
+	 * @param gitPath
+	 *            the git path of the entry, if known already; otherwise
+	 *            {@code null} and it's read from the entry itself
+	 * @throws IOException
+	 *             if the gitlink cannot be checked out
+	 */
+	public void checkoutGitlink(DirCacheEntry entry, String gitPath)
+			throws IOException {
+		FS fs = cache.getRepository().getFS();
+		File workingTree = cache.getRepository().getWorkTree();
+		String path = gitPath != null ? gitPath : entry.getPathString();
+		File gitlinkDir = new File(workingTree, path);
+		File parentDir = gitlinkDir.getParentFile();
+		CacheItem cachedParent = cache.safeCreateDirectory(path, parentDir,
+				false);
+		FileUtils.mkdirs(gitlinkDir, true);
+		cachedParent.insert(path.substring(path.lastIndexOf('/') + 1),
+				FileMode.GITLINK);
+		entry.setLastModified(fs.lastModifiedInstant(gitlinkDir));
+	}
+
+	/**
+	 * Checks out the file given by the {@link DirCacheEntry}.
+	 *
+	 * @param entry
+	 *            {@link DirCacheEntry} to check out
+	 * @param metadata
+	 *            {@link CheckoutMetadata} to use for CR/LF handling and
+	 *            smudge filtering
+	 * @param reader
+	 *            {@link ObjectReader} to use
+	 * @param gitPath
+	 *            the git path of the entry, if known already; otherwise
+	 *            {@code null} and it's read from the entry itself
+	 * @throws IOException
+	 *             if the file cannot be checked out
+	 */
+	public void checkout(DirCacheEntry entry, CheckoutMetadata metadata,
+			ObjectReader reader, String gitPath) throws IOException {
+		if (metadata == null) {
+			metadata = CheckoutMetadata.EMPTY;
+		}
+		FS fs = cache.getRepository().getFS();
+		ObjectLoader ol = reader.open(entry.getObjectId());
+		String path = gitPath != null ? gitPath : entry.getPathString();
+		File f = new File(cache.getRepository().getWorkTree(), path);
+		File parentDir = f.getParentFile();
+		CacheItem cachedParent = cache.safeCreateDirectory(path, parentDir,
+				true);
+		if (entry.getFileMode() == FileMode.SYMLINK
+				&& options.getSymLinks() == SymLinks.TRUE) {
+			byte[] bytes = ol.getBytes();
+			String target = RawParseUtils.decode(bytes);
+			if (recursiveDelete && Files.isDirectory(f.toPath(),
+					LinkOption.NOFOLLOW_LINKS)) {
+				FileUtils.delete(f, FileUtils.RECURSIVE);
+			}
+			fs.createSymLink(f, target);
+			cachedParent.insert(f.getName(), FileMode.SYMLINK);
+			entry.setLength(bytes.length);
+			entry.setLastModified(fs.lastModifiedInstant(f));
+			return;
+		}
+
+		String name = f.getName();
+		if (name.length() > 200) {
+			name = name.substring(0, 200);
+		}
+		File tmpFile = File.createTempFile("._" + name, null, parentDir); //$NON-NLS-1$
+
+		DirCacheCheckout.getContent(cache.getRepository(), path, metadata, ol,
+				options,
+				new FileOutputStream(tmpFile));
+
+		// The entry needs to correspond to the on-disk file size. If the
+		// content was filtered (either by autocrlf handling or smudge
+		// filters) ask the file system again for the length. Otherwise the
+		// object loader knows the size
+		if (metadata.eolStreamType == EolStreamType.DIRECT
+				&& metadata.smudgeFilterCommand == null) {
+			entry.setLength(ol.getSize());
+		} else {
+			entry.setLength(tmpFile.length());
+		}
+
+		if (options.isFileMode() && fs.supportsExecute()) {
+			if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) {
+				if (!fs.canExecute(tmpFile))
+					fs.setExecute(tmpFile, true);
+			} else {
+				if (fs.canExecute(tmpFile))
+					fs.setExecute(tmpFile, false);
+			}
+		}
+		try {
+			if (recursiveDelete && Files.isDirectory(f.toPath(),
+					LinkOption.NOFOLLOW_LINKS)) {
+				FileUtils.delete(f, FileUtils.RECURSIVE);
+			}
+			FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE);
+			cachedParent.remove(f.getName());
+		} catch (IOException e) {
+			throw new IOException(
+					MessageFormat.format(JGitText.get().renameFileFailed,
+							tmpFile.getPath(), f.getPath()),
+					e);
+		} finally {
+			if (tmpFile.exists()) {
+				FileUtils.delete(tmpFile);
+			}
+		}
+		entry.setLastModified(fs.lastModifiedInstant(f));
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index c904a78..1925fd2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -18,10 +18,8 @@
 import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.time.Instant;
 import java.util.ArrayList;
@@ -47,7 +45,6 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
 import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
-import org.eclipse.jgit.lib.CoreConfig.SymLinks;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectChecker;
@@ -67,7 +64,6 @@
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.ExecutionResult;
-import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.IntList;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.SystemReader;
@@ -142,6 +138,8 @@ public CheckoutMetadata(EolStreamType eolStreamType,
 
 	private boolean performingCheckout;
 
+	private Checkout checkout;
+
 	private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
 
 	/**
@@ -492,6 +490,7 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 			CheckoutConflictException, IndexWriteException, CanceledException {
 		toBeDeleted.clear();
 		try (ObjectReader objectReader = repo.getObjectDatabase().newReader()) {
+			checkout = new Checkout(repo, null);
 			if (headCommitTree != null)
 				preScanTwoTrees();
 			else
@@ -558,9 +557,9 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 					CheckoutMetadata meta = e.getValue();
 					DirCacheEntry entry = dc.getEntry(path);
 					if (FileMode.GITLINK.equals(entry.getRawMode())) {
-						checkoutGitlink(path, entry);
+						checkout.checkoutGitlink(entry, path);
 					} else {
-						checkoutEntry(repo, entry, objectReader, false, meta);
+						checkout.checkout(entry, meta, objectReader, path);
 					}
 					e = null;
 
@@ -595,8 +594,8 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 							break;
 						}
 						if (entry.getStage() == DirCacheEntry.STAGE_3) {
-							checkoutEntry(repo, entry, objectReader, false,
-									null);
+							checkout.checkout(entry, null, objectReader,
+									conflict);
 							break;
 						}
 						++entryIdx;
@@ -619,14 +618,6 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 		return toBeDeleted.isEmpty();
 	}
 
-	private void checkoutGitlink(String path, DirCacheEntry entry)
-			throws IOException {
-		File gitlinkDir = new File(repo.getWorkTree(), path);
-		FileUtils.mkdirs(gitlinkDir, true);
-		FS fs = repo.getFS();
-		entry.setLastModified(fs.lastModifiedInstant(gitlinkDir));
-	}
-
 	private static ArrayList<String> filterOut(ArrayList<String> strings,
 			IntList indicesToRemove) {
 		int n = indicesToRemove.size();
@@ -1225,10 +1216,11 @@ private void keep(String path, DirCacheEntry e, WorkingTreeIterator f)
 		if (force) {
 			if (f == null || f.isModified(e, true, walk.getObjectReader())) {
 				kept.add(path);
-				checkoutEntry(repo, e, walk.getObjectReader(), false,
+				checkout.checkout(e,
 						new CheckoutMetadata(walk.getEolStreamType(CHECKOUT_OP),
 								walk.getFilterCommand(
-										Constants.ATTR_FILTER_TYPE_SMUDGE)));
+										Constants.ATTR_FILTER_TYPE_SMUDGE)),
+						walk.getObjectReader(), path);
 			}
 		}
 	}
@@ -1453,76 +1445,9 @@ public static void checkoutEntry(Repository repo, DirCacheEntry entry,
 	public static void checkoutEntry(Repository repo, DirCacheEntry entry,
 			ObjectReader or, boolean deleteRecursive,
 			CheckoutMetadata checkoutMetadata) throws IOException {
-		if (checkoutMetadata == null)
-			checkoutMetadata = CheckoutMetadata.EMPTY;
-		ObjectLoader ol = or.open(entry.getObjectId());
-		File f = new File(repo.getWorkTree(), entry.getPathString());
-		File parentDir = f.getParentFile();
-		if (parentDir.isFile()) {
-			FileUtils.delete(parentDir);
-		}
-		FileUtils.mkdirs(parentDir, true);
-		FS fs = repo.getFS();
-		WorkingTreeOptions opt = repo.getConfig().get(WorkingTreeOptions.KEY);
-		if (entry.getFileMode() == FileMode.SYMLINK
-				&& opt.getSymLinks() == SymLinks.TRUE) {
-			byte[] bytes = ol.getBytes();
-			String target = RawParseUtils.decode(bytes);
-			if (deleteRecursive && f.isDirectory()) {
-				FileUtils.delete(f, FileUtils.RECURSIVE);
-			}
-			fs.createSymLink(f, target);
-			entry.setLength(bytes.length);
-			entry.setLastModified(fs.lastModifiedInstant(f));
-			return;
-		}
-
-		String name = f.getName();
-		if (name.length() > 200) {
-			name = name.substring(0, 200);
-		}
-		File tmpFile = File.createTempFile(
-				"._" + name, null, parentDir); //$NON-NLS-1$
-
-		getContent(repo, entry.getPathString(), checkoutMetadata, ol, opt,
-				new FileOutputStream(tmpFile));
-
-		// The entry needs to correspond to the on-disk filesize. If the content
-		// was filtered (either by autocrlf handling or smudge filters) ask the
-		// filesystem again for the length. Otherwise the objectloader knows the
-		// size
-		if (checkoutMetadata.eolStreamType == EolStreamType.DIRECT
-				&& checkoutMetadata.smudgeFilterCommand == null) {
-			entry.setLength(ol.getSize());
-		} else {
-			entry.setLength(tmpFile.length());
-		}
-
-		if (opt.isFileMode() && fs.supportsExecute()) {
-			if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) {
-				if (!fs.canExecute(tmpFile))
-					fs.setExecute(tmpFile, true);
-			} else {
-				if (fs.canExecute(tmpFile))
-					fs.setExecute(tmpFile, false);
-			}
-		}
-		try {
-			if (deleteRecursive && f.isDirectory()) {
-				FileUtils.delete(f, FileUtils.RECURSIVE);
-			}
-			FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE);
-		} catch (IOException e) {
-			throw new IOException(
-					MessageFormat.format(JGitText.get().renameFileFailed,
-							tmpFile.getPath(), f.getPath()),
-					e);
-		} finally {
-			if (tmpFile.exists()) {
-				FileUtils.delete(tmpFile);
-			}
-		}
-		entry.setLastModified(fs.lastModifiedInstant(f));
+		Checkout checkout = new Checkout(repo, null)
+				.setRecursiveDeletion(deleteRecursive);
+		checkout.checkout(entry, checkoutMetadata, or, null);
 	}
 
 	/**
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 76340da..d6b2a96 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -41,6 +41,8 @@ public static JGitText get() {
 	/***/ public String aNewObjectIdIsRequired;
 	/***/ public String anExceptionOccurredWhileTryingToAddTheIdOfHEAD;
 	/***/ public String anSSHSessionHasBeenAlreadyCreated;
+	/***/ public String applyPatchDestInvalid;
+	/***/ public String applyPatchSourceInvalid;
 	/***/ public String applyBinaryBaseOidWrong;
 	/***/ public String applyBinaryOidTooShort;
 	/***/ public String applyBinaryResultOidWrong;
@@ -571,6 +573,8 @@ public static JGitText get() {
 	/***/ public String packWriterStatistics;
 	/***/ public String panicCantRenameIndexFile;
 	/***/ public String patchApplyException;
+	/***/ public String patchApplyErrorWithHunk;
+	/***/ public String patchApplyErrorWithoutHunk;
 	/***/ public String patchFormatException;
 	/***/ public String pathNotConfigured;
 	/***/ public String peeledLineBeforeRef;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java
new file mode 100644
index 0000000..073bf7a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023, 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.lib;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+
+/**
+ * A hierarchical cache of {@link FileMode}s per git path.
+ *
+ * @since 6.6.1
+ */
+public class FileModeCache {
+
+	@NonNull
+	private final CacheItem root = new CacheItem(FileMode.TREE);
+
+	@NonNull
+	private final Repository repo;
+
+	/**
+	 * Creates a new {@link FileModeCache} for a {@link Repository}.
+	 *
+	 * @param repo
+	 *            {@link Repository} this cache is for
+	 */
+	public FileModeCache(@NonNull Repository repo) {
+		this.repo = repo;
+	}
+
+	/**
+	 * Retrieves the {@link Repository}.
+	 *
+	 * @return the {@link Repository} this {@link FileModeCache} was created for
+	 */
+	@NonNull
+	public Repository getRepository() {
+		return repo;
+	}
+
+	/**
+	 * Obtains the {@link CacheItem} for the working tree root.
+	 *
+	 * @return the {@link CacheItem}
+	 */
+	@NonNull
+	public CacheItem getRoot() {
+		return root;
+	}
+
+	/**
+	 * Ensure that the given parent directory exists, and cache the information
+	 * that gitPath refers to a file.
+	 *
+	 * @param gitPath
+	 *            of the file to be written
+	 * @param parentDir
+	 *            directory in which the file shall be placed, assumed to be the
+	 *            parent of the {@code gitPath}
+	 * @param makeSpace
+	 *            whether to delete a possibly existing file at
+	 *            {@code parentDir}
+	 * @throws IOException
+	 *             if the directory cannot be created, if necessary
+	 */
+	public void safeCreateParentDirectory(String gitPath, File parentDir,
+			boolean makeSpace) throws IOException {
+		CacheItem cachedParent = safeCreateDirectory(gitPath, parentDir,
+				makeSpace);
+		cachedParent.remove(gitPath.substring(gitPath.lastIndexOf('/') + 1));
+	}
+
+	/**
+	 * Ensures the given directory {@code dir} with the given git path exists.
+	 *
+	 * @param gitPath
+	 *            of a file to be written
+	 * @param dir
+	 *            directory in which the file shall be placed, assumed to be the
+	 *            parent of the {@code gitPath}
+	 * @param makeSpace
+	 *            whether to remove a file that already at that name
+	 * @return A {@link CacheItem} describing the directory, which is guaranteed
+	 *         to exist
+	 * @throws IOException
+	 *             if the directory cannot be made to exist at the given
+	 *             location
+	 */
+	public CacheItem safeCreateDirectory(String gitPath, File dir,
+			boolean makeSpace) throws IOException {
+		FS fs = repo.getFS();
+		int i = gitPath.lastIndexOf('/');
+		String parentPath = null;
+		if (i >= 0) {
+			if ((makeSpace && dir.isFile()) || fs.isSymLink(dir)) {
+				FileUtils.delete(dir);
+			}
+			parentPath = gitPath.substring(0, i);
+			deleteSymlinkParent(fs, parentPath, repo.getWorkTree());
+		}
+		FileUtils.mkdirs(dir, true);
+		CacheItem cachedParent = getRoot();
+		if (parentPath != null) {
+			cachedParent = add(parentPath, FileMode.TREE);
+		}
+		return cachedParent;
+	}
+
+	private void deleteSymlinkParent(FS fs, String gitPath, File workingTree)
+			throws IOException {
+		if (!fs.supportsSymlinks()) {
+			return;
+		}
+		String[] parts = gitPath.split("/"); //$NON-NLS-1$
+		int n = parts.length;
+		CacheItem cached = getRoot();
+		File p = workingTree;
+		for (int i = 0; i < n; i++) {
+			p = new File(p, parts[i]);
+			CacheItem cachedChild = cached != null ? cached.child(parts[i])
+					: null;
+			boolean delete = false;
+			if (cachedChild != null) {
+				if (FileMode.SYMLINK.equals(cachedChild.getMode())) {
+					delete = true;
+				}
+			} else {
+				try {
+					Path nioPath = FileUtils.toPath(p);
+					BasicFileAttributes attributes = nioPath.getFileSystem()
+							.provider()
+							.getFileAttributeView(nioPath,
+									BasicFileAttributeView.class,
+									LinkOption.NOFOLLOW_LINKS)
+							.readAttributes();
+					if (attributes.isSymbolicLink()) {
+						delete = p.isDirectory();
+					} else if (attributes.isRegularFile()) {
+						break;
+					}
+				} catch (InvalidPathException | IOException e) {
+					// If we can't get the attributes the path does not exist,
+					// or if it does a subsequent mkdirs() will also throw an
+					// exception.
+					break;
+				}
+			}
+			if (delete) {
+				// Deletes the symlink
+				FileUtils.delete(p, FileUtils.SKIP_MISSING);
+				if (cached != null) {
+					cached.remove(parts[i]);
+				}
+				break;
+			}
+			cached = cachedChild;
+		}
+	}
+
+	/**
+	 * Records the given {@link FileMode} for the given git path in the cache.
+	 * If an entry already exists for the given path, the previously cached file
+	 * mode is overwritten.
+	 *
+	 * @param gitPath
+	 *            to cache the {@link FileMode} for
+	 * @param finalMode
+	 *            {@link FileMode} to cache
+	 * @return the {@link CacheItem} for the path
+	 */
+	@NonNull
+	private CacheItem add(String gitPath, FileMode finalMode) {
+		if (gitPath.isEmpty()) {
+			throw new IllegalArgumentException();
+		}
+		String[] parts = gitPath.split("/"); //$NON-NLS-1$
+		int n = parts.length;
+		int i = 0;
+		CacheItem curr = getRoot();
+		while (i < n) {
+			CacheItem next = curr.child(parts[i]);
+			if (next == null) {
+				break;
+			}
+			curr = next;
+			i++;
+		}
+		if (i == n) {
+			curr.setMode(finalMode);
+		} else {
+			while (i < n) {
+				curr = curr.insert(parts[i],
+						i + 1 == n ? finalMode : FileMode.TREE);
+				i++;
+			}
+		}
+		return curr;
+	}
+
+	/**
+	 * An item from a {@link FileModeCache}, recording information about a git
+	 * path (known from context).
+	 */
+	public static class CacheItem {
+
+		@NonNull
+		private FileMode mode;
+
+		private Map<String, CacheItem> children;
+
+		/**
+		 * Creates a new {@link CacheItem}.
+		 *
+		 * @param mode
+		 *            {@link FileMode} to cache
+		 */
+		public CacheItem(@NonNull FileMode mode) {
+			this.mode = mode;
+		}
+
+		/**
+		 * Retrieves the cached {@link FileMode}.
+		 *
+		 * @return the {@link FileMode}
+		 */
+		@NonNull
+		public FileMode getMode() {
+			return mode;
+		}
+
+		/**
+		 * Retrieves an immediate child of this {@link CacheItem} by name.
+		 *
+		 * @param childName
+		 *            name of the child to get
+		 * @return the {@link CacheItem}, or {@code null} if no such child is
+		 *         known
+		 */
+		public CacheItem child(String childName) {
+			if (children == null) {
+				return null;
+			}
+			return children.get(childName);
+		}
+
+		/**
+		 * Inserts a new cached {@link FileMode} as an immediate child of this
+		 * {@link CacheItem}. If there is already a child with the same name, it
+		 * is overwritten.
+		 *
+		 * @param childName
+		 *            name of the child to create
+		 * @param childMode
+		 *            {@link FileMode} to cache
+		 * @return the new {@link CacheItem} created for the child
+		 */
+		public CacheItem insert(String childName, @NonNull FileMode childMode) {
+			if (!FileMode.TREE.equals(mode)) {
+				throw new IllegalArgumentException();
+			}
+			if (children == null) {
+				children = new HashMap<>();
+			}
+			CacheItem newItem = new CacheItem(childMode);
+			children.put(childName, newItem);
+			return newItem;
+		}
+
+		/**
+		 * Removes the immediate child with the given name.
+		 *
+		 * @param childName
+		 *            name of the child to remove
+		 * @return the previously cached {@link CacheItem}, if any
+		 */
+		public CacheItem remove(String childName) {
+			if (children == null) {
+				return null;
+			}
+			return children.remove(childName);
+		}
+
+		void setMode(@NonNull FileMode mode) {
+			this.mode = mode;
+			if (!FileMode.TREE.equals(mode)) {
+				children = null;
+			}
+		}
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 7767662..3b778a3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -44,10 +44,10 @@
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.diff.RawTextComparator;
 import org.eclipse.jgit.diff.Sequence;
+import org.eclipse.jgit.dircache.Checkout;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuildIterator;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
-import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.errors.BinaryBlobException;
@@ -76,7 +76,6 @@
 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
 import org.eclipse.jgit.treewalk.WorkingTreeOptions;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
-import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.LfsFactory;
 import org.eclipse.jgit.util.LfsFactory.LfsInputStream;
 import org.eclipse.jgit.util.TemporaryBuffer;
@@ -86,6 +85,13 @@
  * A three-way merger performing a content-merge if necessary
  */
 public class ResolveMerger extends ThreeWayMerger {
+
+	/**
+	 * {@link Checkout} to use for actually checking out files if
+	 * {@link #inCore} is {@code false}.
+	 */
+	private Checkout checkout;
+
 	/**
 	 * If the merge fails (means: not stopped because of unresolved conflicts)
 	 * this enum is used to explain why it failed
@@ -322,6 +328,7 @@ protected ResolveMerger(Repository local, boolean inCore) {
 			implicitDirCache = true;
 			workingTreeOptions = local.getConfig().get(WorkingTreeOptions.KEY);
 		}
+		checkout = new Checkout(nonNullRepo(), workingTreeOptions);
 	}
 
 	/**
@@ -411,12 +418,15 @@ private void checkout() throws NoWorkTreeException, IOException {
 		for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut
 				.entrySet()) {
 			DirCacheEntry cacheEntry = entry.getValue();
+			String gitPath = entry.getKey();
 			if (cacheEntry.getFileMode() == FileMode.GITLINK) {
 				new File(nonNullRepo().getWorkTree(), entry.getKey()).mkdirs();
+				checkout.checkoutGitlink(cacheEntry, gitPath);
 			} else {
-				DirCacheCheckout.checkoutEntry(db, cacheEntry, reader, false,
-						checkoutMetadata.get(entry.getKey()));
-				modifiedFiles.add(entry.getKey());
+				checkout.checkout(cacheEntry,
+						checkoutMetadata.get(entry.getKey()), reader,
+						gitPath);
+				modifiedFiles.add(gitPath);
 			}
 		}
 	}
@@ -446,8 +456,8 @@ protected void cleanUp() throws NoWorkTreeException,
 			String mpath = mpathsIt.next();
 			DirCacheEntry entry = dc.getEntry(mpath);
 			if (entry != null) {
-				DirCacheCheckout.checkoutEntry(db, entry, reader, false,
-						checkoutMetadata.get(mpath));
+				checkout.checkout(entry, checkoutMetadata.get(mpath),
+						reader, mpath);
 			}
 			mpathsIt.remove();
 		}
@@ -1076,15 +1086,12 @@ private File writeMergedFile(TemporaryBuffer rawMerged,
 			Attributes attributes)
 			throws FileNotFoundException, IOException {
 		File workTree = nonNullRepo().getWorkTree();
-		FS fs = nonNullRepo().getFS();
-		File of = new File(workTree, tw.getPathString());
-		File parentFolder = of.getParentFile();
-		if (!fs.exists(parentFolder)) {
-			parentFolder.mkdirs();
-		}
+		String gitPath = tw.getPathString();
+		File of = new File(workTree, gitPath);
 		EolStreamType streamType = EolStreamTypeUtil.detectStreamType(
 				OperationType.CHECKOUT_OP, workingTreeOptions,
 				attributes);
+		checkout.safeCreateParentDirectory(tw.getPathString(), of.getParentFile(), false);
 		try (OutputStream os = EolStreamTypeUtil.wrapOutputStream(
 				new BufferedOutputStream(new FileOutputStream(of)),
 				streamType)) {
@@ -1362,9 +1369,9 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree,
 			// go into the new index.
 			checkout();
 
-			// All content-merges are successfully done. If we can now write the
-			// new index we are on quite safe ground. Even if the checkout of
-			// files coming from "theirs" fails the user can work around such
+			// All content-merges are successfully done. If we can now write
+			// the new index we are on quite safe ground. Even if the checkout
+			// of files coming from "theirs" fails the user can work around such
 			// failures by checking out the index again.
 			if (!builder.commit()) {
 				cleanUp();
-- 
2.33.0

