ApplyCommand: convert to git internal format before applying patch

Applying a patch on Windows failed if the patch had the (normal)
single-LF line endings, but the file on disk had the usual Windows
CR-LF line endings.

Git (and JGit) compute diffs on the git-internal blob, i.e., after
CR-LF transformation and clean filtering. Applying patches to files
directly is thus incorrect and may fail if CR-LF settings don't
match, or if clean/smudge filtering is involved.

Change ApplyCommand to run the file content through the check-in
filters before applying the patch, and run the result through the
check-out filters. This makes patch application succeed even if the
patch has single-LFs, but the file has CR-LF and core.autocrlf is
true.

Add tests for various combinations of line endings in the file and in
the patch, and a test to verify the clean/smudge handling.

See also [1].

Running the file though clean/smudge may give strange results with
LFS-managed files. JGit's DiffFormatter has some extra code and
applies the smudge filter again after having run the file through
the check-in filters (CR-LF and clean). So JGit can actually produce
a diff on LFS-managed files using the normal diff machinery. (If it
doesn't run out of memory, that is. After all, LFS is intended for
_large_ files.) How such a diff would be applied with either C git
or JGit is entirely unclear; neither has any code for this special
case. Compare also [2].

Note that C git just doesn't know about LFS and always diffs after
the check-in filter chain, so for LFS files, it'll produce a diff
of the LFS pointers.

[1] https://github.com/git/git/commit/c24f3abac
[2] https://github.com/git-lfs/git-lfs/issues/440

Bug: 571585
Change-Id: I8f71ff26313b5773ff1da612b0938ad2f18751f5
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf.patch
new file mode 100644
index 0000000..01eb0b9
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf.patch
@@ -0,0 +1,9 @@
+diff --git a/crlf b/crlf
+index 9206ee6..95dd193 100644
+--- a/crlf
++++ b/crlf
+@@ -1,3 +1,3 @@
+ foo
+-fie
++bar
+ fum
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2.patch
new file mode 100644
index 0000000..5a62104
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2.patch
@@ -0,0 +1,9 @@
+diff --git a/crlf2 b/crlf2
+index 05c1c78..91e246d 100644
+--- a/crlf2
++++ b/crlf2
+@@ -1,3 +1,3 @@
+ foo

+-fie

++bar

+ fum

diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2_PostImage
new file mode 100644
index 0000000..91e246d
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2_PostImage
@@ -0,0 +1,3 @@
+foo

+bar

+fum

diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2_PreImage
new file mode 100644
index 0000000..05c1c78
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf2_PreImage
@@ -0,0 +1,3 @@
+foo

+fie

+fum

diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3.patch
new file mode 100644
index 0000000..b155148
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3.patch
@@ -0,0 +1,8 @@
+diff --git a/crlf3 b/crlf3
+index e69de29..9206ee6 100644
+--- a/crlf3
++++ b/crlf3
+@@ -0,0 +1,3 @@
++foo
++fie
++fum
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3_PostImage
new file mode 100644
index 0000000..05c1c78
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3_PostImage
@@ -0,0 +1,3 @@
+foo

+fie

+fum

diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3_PreImage
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf3_PreImage
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf4.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf4.patch
new file mode 100644
index 0000000..0cf6063
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf4.patch
@@ -0,0 +1,9 @@
+diff --git a/crlf4 b/crlf4
+new file mode 100644
+index 0000000..9206ee6
+--- /dev/null
++++ b/crlf4
+@@ -0,0 +1,3 @@
++foo
++fie
++fum
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf4_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf4_PostImage
new file mode 100644
index 0000000..05c1c78
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf4_PostImage
@@ -0,0 +1,3 @@
+foo

+fie

+fum

diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf_PostImage
new file mode 100644
index 0000000..91e246d
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf_PostImage
@@ -0,0 +1,3 @@
+foo

+bar

+fum

diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf_PreImage
new file mode 100644
index 0000000..05c1c78
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/crlf_PreImage
@@ -0,0 +1,3 @@
+foo

+fie

+fum

diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest.patch
new file mode 100644
index 0000000..ab4d426
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest.patch
@@ -0,0 +1,9 @@
+diff --git a/smudgetest b/smudgetest
+index a24d41e..762c4d0 100644
+--- a/smudgetest
++++ b/smudgetest
+@@ -1,3 +1,3 @@
+ PERLE
+-HEBLE
++sprich
+ speak
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest_PostImage
new file mode 100644
index 0000000..ad63089
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest_PostImage
@@ -0,0 +1,3 @@
+PARLA
+sprich
+speak
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest_PreImage
new file mode 100644
index 0000000..9bbd8c7
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/smudgetest_PreImage
@@ -0,0 +1,3 @@
+PARLA
+HABLA
+speak
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java
index 055eba7..335a64d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, 2020 IBM Corporation and others
+ * Copyright (C) 2011, 2021 IBM Corporation 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
@@ -18,11 +18,17 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 
 import org.eclipse.jgit.api.errors.PatchApplyException;
 import org.eclipse.jgit.api.errors.PatchFormatException;
+import org.eclipse.jgit.attributes.FilterCommand;
+import org.eclipse.jgit.attributes.FilterCommandFactory;
+import org.eclipse.jgit.attributes.FilterCommandRegistry;
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.junit.Test;
 
 public class ApplyCommandTest extends RepositoryTestCase {
@@ -58,6 +64,189 @@
 	}
 
 	@Test
+	public void testCrLf() throws Exception {
+		try {
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
+			ApplyResult result = init("crlf", true, true);
+			assertEquals(1, result.getUpdatedFiles().size());
+			assertEquals(new File(db.getWorkTree(), "crlf"),
+					result.getUpdatedFiles().get(0));
+			checkFile(new File(db.getWorkTree(), "crlf"),
+					b.getString(0, b.size(), false));
+		} finally {
+			db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF);
+		}
+	}
+
+	@Test
+	public void testCrLfOff() throws Exception {
+		try {
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF, false);
+			ApplyResult result = init("crlf", true, true);
+			assertEquals(1, result.getUpdatedFiles().size());
+			assertEquals(new File(db.getWorkTree(), "crlf"),
+					result.getUpdatedFiles().get(0));
+			checkFile(new File(db.getWorkTree(), "crlf"),
+					b.getString(0, b.size(), false));
+		} finally {
+			db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF);
+		}
+	}
+
+	@Test
+	public void testCrLfEmptyCommitted() throws Exception {
+		try {
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
+			ApplyResult result = init("crlf3", true, true);
+			assertEquals(1, result.getUpdatedFiles().size());
+			assertEquals(new File(db.getWorkTree(), "crlf3"),
+					result.getUpdatedFiles().get(0));
+			checkFile(new File(db.getWorkTree(), "crlf3"),
+					b.getString(0, b.size(), false));
+		} finally {
+			db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF);
+		}
+	}
+
+	@Test
+	public void testCrLfNewFile() throws Exception {
+		try {
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
+			ApplyResult result = init("crlf4", false, true);
+			assertEquals(1, result.getUpdatedFiles().size());
+			assertEquals(new File(db.getWorkTree(), "crlf4"),
+					result.getUpdatedFiles().get(0));
+			checkFile(new File(db.getWorkTree(), "crlf4"),
+					b.getString(0, b.size(), false));
+		} finally {
+			db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF);
+		}
+	}
+
+	@Test
+	public void testPatchWithCrLf() throws Exception {
+		try {
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF, false);
+			ApplyResult result = init("crlf2", true, true);
+			assertEquals(1, result.getUpdatedFiles().size());
+			assertEquals(new File(db.getWorkTree(), "crlf2"),
+					result.getUpdatedFiles().get(0));
+			checkFile(new File(db.getWorkTree(), "crlf2"),
+					b.getString(0, b.size(), false));
+		} finally {
+			db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF);
+		}
+	}
+
+	@Test
+	public void testPatchWithCrLf2() throws Exception {
+		String name = "crlf2";
+		try (Git git = new Git(db)) {
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF, false);
+			a = new RawText(readFile(name + "_PreImage"));
+			write(new File(db.getWorkTree(), name),
+					a.getString(0, a.size(), false));
+
+			git.add().addFilepattern(name).call();
+			git.commit().setMessage("PreImage").call();
+
+			b = new RawText(readFile(name + "_PostImage"));
+
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
+			ApplyResult result = git.apply()
+					.setPatch(getTestResource(name + ".patch")).call();
+			assertEquals(1, result.getUpdatedFiles().size());
+			assertEquals(new File(db.getWorkTree(), name),
+					result.getUpdatedFiles().get(0));
+			checkFile(new File(db.getWorkTree(), name),
+					b.getString(0, b.size(), false));
+		} finally {
+			db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_AUTOCRLF);
+		}
+	}
+
+	// Clean/smudge filter for testFiltering. The smudgetest test resources were
+	// created with C git using a clean filter sed -e "s/A/E/g" and the smudge
+	// filter sed -e "s/E/A/g". To keep the test independent of the presence of
+	// sed, implement this with a built-in filter.
+	private static class ReplaceFilter extends FilterCommand {
+
+		private final char toReplace;
+
+		private final char replacement;
+
+		ReplaceFilter(InputStream in, OutputStream out, char toReplace,
+				char replacement) {
+			super(in, out);
+			this.toReplace = toReplace;
+			this.replacement = replacement;
+		}
+
+		@Override
+		public int run() throws IOException {
+			int b = in.read();
+			if (b < 0) {
+				in.close();
+				out.close();
+				return -1;
+			}
+			if ((b & 0xFF) == toReplace) {
+				b = replacement;
+			}
+			out.write(b);
+			return 1;
+		}
+	}
+
+	@Test
+	public void testFiltering() throws Exception {
+		// Set up filter
+		FilterCommandFactory clean = (repo, in, out) -> {
+			return new ReplaceFilter(in, out, 'A', 'E');
+		};
+		FilterCommandFactory smudge = (repo, in, out) -> {
+			return new ReplaceFilter(in, out, 'E', 'A');
+		};
+		FilterCommandRegistry.register("jgit://builtin/a2e/clean", clean);
+		FilterCommandRegistry.register("jgit://builtin/a2e/smudge", smudge);
+		try (Git git = new Git(db)) {
+			Config config = db.getConfig();
+			config.setString(ConfigConstants.CONFIG_FILTER_SECTION, "a2e",
+					"clean", "jgit://builtin/a2e/clean");
+			config.setString(ConfigConstants.CONFIG_FILTER_SECTION, "a2e",
+					"smudge", "jgit://builtin/a2e/smudge");
+			write(new File(db.getWorkTree(), ".gitattributes"),
+					"smudgetest filter=a2e");
+			git.add().addFilepattern(".gitattributes").call();
+			git.commit().setMessage("Attributes").call();
+			ApplyResult result = init("smudgetest", true, true);
+			assertEquals(1, result.getUpdatedFiles().size());
+			assertEquals(new File(db.getWorkTree(), "smudgetest"),
+					result.getUpdatedFiles().get(0));
+			checkFile(new File(db.getWorkTree(), "smudgetest"),
+					b.getString(0, b.size(), false));
+
+		} finally {
+			// Tear down filter
+			FilterCommandRegistry.unregister("jgit://builtin/a2e/clean");
+			FilterCommandRegistry.unregister("jgit://builtin/a2e/smudge");
+		}
+	}
+
+	@Test
 	public void testAddA1() throws Exception {
 		ApplyResult result = init("A1", false, true);
 		assertEquals(1, result.getUpdatedFiles().size());
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 e228e82..5d975ea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, 2020 IBM Corporation and others
+ * Copyright (C) 2011, 2021 IBM Corporation 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
@@ -9,10 +9,16 @@
  */
 package org.eclipse.jgit.api;
 
+import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.io.Writer;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
@@ -20,18 +26,45 @@
 import java.util.Iterator;
 import java.util.List;
 
+import org.eclipse.jgit.api.errors.FilterFailedException;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.PatchApplyException;
 import org.eclipse.jgit.api.errors.PatchFormatException;
+import org.eclipse.jgit.attributes.FilterCommand;
+import org.eclipse.jgit.attributes.FilterCommandRegistry;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectStream;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.patch.HunkHeader;
 import org.eclipse.jgit.patch.Patch;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
+import org.eclipse.jgit.util.io.EolStreamTypeUtil;
 
 /**
  * Apply a patch to files and/or to the index.
@@ -45,7 +78,7 @@
 	private InputStream in;
 
 	/**
-	 * Constructs the command if the patch is to be applied to the index.
+	 * Constructs the command.
 	 *
 	 * @param repo
 	 */
@@ -79,6 +112,7 @@
 	public ApplyResult call() throws GitAPIException, PatchFormatException,
 			PatchApplyException {
 		checkCallable();
+		setCallable(false);
 		ApplyResult r = new ApplyResult();
 		try {
 			final Patch p = new Patch();
@@ -87,19 +121,22 @@
 			} finally {
 				in.close();
 			}
-			if (!p.getErrors().isEmpty())
+			if (!p.getErrors().isEmpty()) {
 				throw new PatchFormatException(p.getErrors());
+			}
+			Repository repository = getRepository();
+			DirCache cache = repository.readDirCache();
 			for (FileHeader fh : p.getFiles()) {
 				ChangeType type = fh.getChangeType();
 				File f = null;
 				switch (type) {
 				case ADD:
 					f = getFile(fh.getNewPath(), true);
-					apply(f, fh);
+					apply(repository, fh.getNewPath(), cache, f, fh);
 					break;
 				case MODIFY:
 					f = getFile(fh.getOldPath(), false);
-					apply(f, fh);
+					apply(repository, fh.getOldPath(), cache, f, fh);
 					break;
 				case DELETE:
 					f = getFile(fh.getOldPath(), false);
@@ -118,14 +155,14 @@
 						throw new PatchApplyException(MessageFormat.format(
 								JGitText.get().renameFileFailed, f, dest), e);
 					}
-					apply(dest, fh);
+					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);
 					Files.copy(f.toPath(), target.toPath());
-					apply(target, fh);
+					apply(repository, fh.getOldPath(), cache, target, fh);
 				}
 				r.addUpdatedFile(f);
 			}
@@ -133,14 +170,13 @@
 			throw new PatchApplyException(MessageFormat.format(
 					JGitText.get().patchApplyException, e.getMessage()), e);
 		}
-		setCallable(false);
 		return r;
 	}
 
 	private File getFile(String path, boolean create)
 			throws PatchApplyException {
 		File f = new File(getRepository().getWorkTree(), path);
-		if (create)
+		if (create) {
 			try {
 				File parent = f.getParentFile();
 				FileUtils.mkdirs(parent, true);
@@ -149,21 +185,201 @@
 				throw new PatchApplyException(MessageFormat.format(
 						JGitText.get().createNewFileFailed, f), e);
 			}
+		}
 		return f;
 	}
 
+	private void apply(Repository repository, String path, DirCache cache,
+			File f, FileHeader fh) throws IOException, PatchApplyException {
+		boolean convertCrLf = needsCrLfConversion(f, fh);
+		// Use a TreeWalk with a DirCacheIterator to pick up the correct
+		// clean/smudge filters. CR-LF handling is completely determined by
+		// whether the file or the patch have CR-LF line endings.
+		try (TreeWalk walk = new TreeWalk(repository)) {
+			walk.setOperationType(OperationType.CHECKIN_OP);
+			FileTreeIterator files = new FileTreeIterator(repository);
+			int fileIdx = walk.addTree(files);
+			int cacheIdx = walk.addTree(new DirCacheIterator(cache));
+			files.setDirCacheIterator(walk, cacheIdx);
+			walk.setFilter(AndTreeFilter.create(
+					PathFilterGroup.createFromStrings(path),
+					new NotIgnoredFilter(fileIdx)));
+			walk.setRecursive(true);
+			if (walk.next()) {
+				// If the file on disk has no newline characters, convertCrLf
+				// will be false. In that case we want to honor the normal git
+				// settings.
+				EolStreamType streamType = convertCrLf ? EolStreamType.TEXT_CRLF
+						: walk.getEolStreamType(OperationType.CHECKOUT_OP);
+				String command = walk.getFilterCommand(
+						Constants.ATTR_FILTER_TYPE_SMUDGE);
+				CheckoutMetadata checkOut = new CheckoutMetadata(streamType, command);
+				FileTreeIterator file = walk.getTree(fileIdx,
+						FileTreeIterator.class);
+				if (file != null) {
+					command = walk
+							.getFilterCommand(Constants.ATTR_FILTER_TYPE_CLEAN);
+					RawText raw;
+					// Can't use file.openEntryStream() as it would do CR-LF
+					// conversion as usual, not as wanted by us.
+					try (InputStream input = filterClean(repository, path,
+							new FileInputStream(f), convertCrLf, command)) {
+						raw = new RawText(IO.readWholeStream(input, 0).array());
+					}
+					apply(repository, path, raw, f, fh, checkOut);
+					return;
+				}
+			}
+		}
+		// File ignored?
+		RawText raw;
+		CheckoutMetadata checkOut;
+		if (convertCrLf) {
+			try (InputStream input = EolStreamTypeUtil.wrapInputStream(
+					new FileInputStream(f), EolStreamType.TEXT_LF)) {
+				raw = new RawText(IO.readWholeStream(input, 0).array());
+			}
+			checkOut = new CheckoutMetadata(EolStreamType.TEXT_CRLF, null);
+		} else {
+			raw = new RawText(f);
+			checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
+		}
+		apply(repository, path, raw, f, fh, checkOut);
+	}
+
+	private boolean needsCrLfConversion(File f, FileHeader fileHeader)
+			throws IOException {
+		if (!hasCrLf(fileHeader)) {
+			try (InputStream input = new FileInputStream(f)) {
+				return RawText.isCrLfText(input);
+			}
+		}
+		return false;
+	}
+
+	private static boolean hasCrLf(FileHeader fileHeader) {
+		if (fileHeader == null) {
+			return false;
+		}
+		for (HunkHeader header : fileHeader.getHunks()) {
+			byte[] buf = header.getBuffer();
+			int hunkEnd = header.getEndOffset();
+			int lineStart = header.getStartOffset();
+			while (lineStart < hunkEnd) {
+				int nextLineStart = RawParseUtils.nextLF(buf, lineStart);
+				if (nextLineStart > hunkEnd) {
+					nextLineStart = hunkEnd;
+				}
+				if (nextLineStart <= lineStart) {
+					break;
+				}
+				if (nextLineStart - lineStart > 1) {
+					char first = (char) (buf[lineStart] & 0xFF);
+					if (first == ' ' || first == '-') {
+						// It's an old line. Does it end in CR-LF?
+						if (buf[nextLineStart - 2] == '\r') {
+							return true;
+						}
+					}
+				}
+				lineStart = nextLineStart;
+			}
+		}
+		return false;
+	}
+
+	private InputStream filterClean(Repository repository, String path,
+			InputStream fromFile, boolean convertCrLf, String filterCommand)
+			throws IOException {
+		InputStream input = fromFile;
+		if (convertCrLf) {
+			input = EolStreamTypeUtil.wrapInputStream(input,
+					EolStreamType.TEXT_LF);
+		}
+		if (StringUtils.isEmptyOrNull(filterCommand)) {
+			return input;
+		}
+		if (FilterCommandRegistry.isRegistered(filterCommand)) {
+			LocalFile buffer = new TemporaryBuffer.LocalFile(null);
+			FilterCommand command = FilterCommandRegistry.createFilterCommand(
+					filterCommand, repository, input, buffer);
+			while (command.run() != -1) {
+				// loop as long as command.run() tells there is work to do
+			}
+			return buffer.openInputStreamWithAutoDestroy();
+		}
+		FS fs = repository.getFS();
+		ProcessBuilder filterProcessBuilder = fs.runInShell(filterCommand,
+				new String[0]);
+		filterProcessBuilder.directory(repository.getWorkTree());
+		filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
+				repository.getDirectory().getAbsolutePath());
+		ExecutionResult result;
+		try {
+			result = fs.execute(filterProcessBuilder, in);
+		} catch (IOException | InterruptedException e) {
+			throw new IOException(
+					new FilterFailedException(e, filterCommand, path));
+		}
+		int rc = result.getRc();
+		if (rc != 0) {
+			throw new IOException(new FilterFailedException(rc, filterCommand,
+					path, result.getStdout().toByteArray(4096), RawParseUtils
+							.decode(result.getStderr().toByteArray(4096))));
+		}
+		return result.getStdout().openInputStreamWithAutoDestroy();
+	}
+
 	/**
-	 * @param f
-	 * @param fh
-	 * @throws IOException
-	 * @throws PatchApplyException
+	 * We write the patch result to a {@link TemporaryBuffer} and then use
+	 * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF
+	 * and smudge filters. DirCacheCheckout needs an ObjectLoader, not a
+	 * TemporaryBuffer, so this class bridges between the two, making the
+	 * TemporaryBuffer look like an ordinary git blob to DirCacheCheckout.
 	 */
-	private void apply(File f, FileHeader fh)
+	private static class BufferLoader extends ObjectLoader {
+
+		private TemporaryBuffer data;
+
+		BufferLoader(TemporaryBuffer data) {
+			this.data = data;
+		}
+
+		@Override
+		public int getType() {
+			return Constants.OBJ_BLOB;
+		}
+
+		@Override
+		public long getSize() {
+			return data.length();
+		}
+
+		@Override
+		public boolean isLarge() {
+			return true;
+		}
+
+		@Override
+		public byte[] getCachedBytes() throws LargeObjectException {
+			throw new LargeObjectException();
+		}
+
+		@Override
+		public ObjectStream openStream()
+				throws MissingObjectException, IOException {
+			return new ObjectStream.Filter(getType(), getSize(),
+					data.openInputStream());
+		}
+	}
+
+	private void apply(Repository repository, String path, RawText rt, File f,
+			FileHeader fh, CheckoutMetadata checkOut)
 			throws IOException, PatchApplyException {
-		RawText rt = new RawText(f);
 		List<String> oldLines = new ArrayList<>(rt.size());
-		for (int i = 0; i < rt.size(); i++)
+		for (int i = 0; i < rt.size(); i++) {
 			oldLines.add(rt.getString(i));
+		}
 		List<String> newLines = new ArrayList<>(oldLines);
 		int afterLastHunk = 0;
 		int lineNumberShift = 0;
@@ -279,17 +495,32 @@
 		if (!isChanged(oldLines, newLines)) {
 			return; // Don't touch the file
 		}
-		try (Writer fw = Files.newBufferedWriter(f.toPath())) {
-			for (Iterator<String> l = newLines.iterator(); l.hasNext();) {
-				fw.write(l.next());
-				if (l.hasNext()) {
-					// Don't bother handling line endings - if it was Windows,
-					// the \r is still there!
-					fw.write('\n');
+
+		// TODO: forcing UTF-8 is a bit strange and may lead to re-coding if the
+		// input was some other encoding, but it's what previous versions of
+		// this code used. (Even earlier the code used the default encoding,
+		// which has the same problem.) Perhaps using bytes instead of Strings
+		// for the lines would be better.
+		TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
+		try {
+			try (Writer w = new BufferedWriter(
+					new OutputStreamWriter(buffer, StandardCharsets.UTF_8))) {
+				for (Iterator<String> l = newLines.iterator(); l.hasNext();) {
+					w.write(l.next());
+					if (l.hasNext()) {
+						w.write('\n');
+					}
 				}
 			}
+			try (OutputStream output = new FileOutputStream(f)) {
+				DirCacheCheckout.getContent(repository, path, checkOut,
+						new BufferLoader(buffer), null, output);
+			}
+		} finally {
+			buffer.destroy();
 		}
-		getRepository().getFS().setExecute(f, fh.getNewMode() == FileMode.EXECUTABLE_FILE);
+		repository.getFS().setExecute(f,
+				fh.getNewMode() == FileMode.EXECUTABLE_FILE);
 	}
 
 	private boolean canApplyAt(List<String> hunkLines, List<String> newLines,