ApplyCommand: use context lines to determine hunk location

If a hunk does not apply at the position stated in the hunk header
try to determine its position using the old lines (context and
deleted lines).

This is still a far cry from a full git apply: it doesn't do binary
patches, it doesn't handle git's whitespace options, and it's perhaps
not the fastest on big patches. C git hashes the lines and uses these
hashes to speed up matching hunks (and to do its whitespace magic).

Bug: 562348
Change-Id: Id0796bba059d84e648769d5896f497fde0b787dd
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown.patch
new file mode 100644
index 0000000..74c3371
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown.patch
@@ -0,0 +1,14 @@
+diff --git a/ShiftDown b/ShiftDown
+index 8b9727b..25dc192 100644
+--- a/ShiftDown
++++ b/ShiftDown
+@@ -16,6 +16,9 @@
+ 			something("A.b", "bar");
+ 		}
+ 
++		public void methodC() {
++			something("A.c", "bar");
++		}
+ 	}
+ 
+ 	public class B {
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2.patch
new file mode 100644
index 0000000..a2b34b3
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2.patch
@@ -0,0 +1,24 @@
+diff --git a/ShiftDown2 b/ShiftDown2
+index 8b9727b..63353aa 100644
+--- a/ShiftDown2
++++ b/ShiftDown2
+@@ -16,6 +16,9 @@
+ 			something("A.b", "bar");
+ 		}
+ 
++		public void methodC() {
++			something("A.c", "bar");
++		}
+ 	}
+ 
+ 	public class B {
+@@ -28,5 +31,9 @@
+ 			something("B.b", "bar");
+ 		}
+ 
++		public void methodC() {
++			something("B.c", "bar");
++		}
++
+ 	}
+ }
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PostImage
new file mode 100644
index 0000000..738484e
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PostImage
@@ -0,0 +1,75 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class D {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class E {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class F {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+		public void methodC() {
+			something("A.c", "bar");
+		}
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+		public void methodC() {
+			something("B.c", "bar");
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PreImage
new file mode 100644
index 0000000..e1ee19c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PreImage
@@ -0,0 +1,68 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class D {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class E {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class F {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PostImage
new file mode 100644
index 0000000..5c6e9bc
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PostImage
@@ -0,0 +1,71 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class D {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class E {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class F {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+		public void methodC() {
+			something("A.c", "bar");
+		}
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PreImage
new file mode 100644
index 0000000..e1ee19c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PreImage
@@ -0,0 +1,68 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class D {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class E {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class F {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp.patch
new file mode 100644
index 0000000..aa994a1
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp.patch
@@ -0,0 +1,14 @@
+diff --git a/ShiftUp b/ShiftUp
+index e1ee19c..5c6e9bc 100644
+--- a/ShiftUp
++++ b/ShiftUp
+@@ -52,6 +52,9 @@
+ 			something("A.b", "bar");
+ 		}
+ 
++		public void methodC() {
++			something("A.c", "bar");
++		}
+ 	}
+ 
+ 	public class B {
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2.patch
new file mode 100644
index 0000000..eca9971
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2.patch
@@ -0,0 +1,23 @@
+diff --git a/ShiftUp2 b/ShiftUp2
+index e1ee19c..f010144 100644
+--- a/ShiftUp2
++++ b/ShiftUp2
+@@ -52,6 +52,9 @@
+ 			something("A.b", "bar");
+ 		}
+ 
++		public void methodC() {
++			something("A.c", "bar");
++		}
+ 	}
+ 
+ 	public class B {
+@@ -64,5 +67,8 @@
+ 			something("B.b", "bar");
+ 		}
+ 
++		public void methodC() {
++			something("B.c", "bar");
++		}
+ 	}
+ }
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PostImage
new file mode 100644
index 0000000..e279ece
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PostImage
@@ -0,0 +1,38 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+		public void methodC() {
+			something("A.c", "bar");
+		}
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+		public void methodC() {
+			something("B.c", "bar");
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PreImage
new file mode 100644
index 0000000..8b9727b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PreImage
@@ -0,0 +1,32 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PostImage
new file mode 100644
index 0000000..25dc192
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PostImage
@@ -0,0 +1,35 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+		public void methodC() {
+			something("A.c", "bar");
+		}
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PreImage
new file mode 100644
index 0000000..8b9727b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PreImage
@@ -0,0 +1,32 @@
+package org.eclipse.jgit.test.apply;
+
+public class TestClass {
+
+	private void something(String prefix, String msg) {
+		System.out.println(prefix + ": " + msg);
+	}
+
+	public class A {
+
+		public void methodA() {
+			something("A.a", "foo");
+		}
+
+		public void methodB() {
+			something("A.b", "bar");
+		}
+
+	}
+
+	public class B {
+
+		public void methodA() {
+			something("B.a", "foo");
+		}
+
+		public void methodB() {
+			something("B.b", "bar");
+		}
+
+	}
+}
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 63cd21f..055eba7 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, 2012, IBM Corporation and others. and others
+ * Copyright (C) 2011, 2020 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
@@ -280,6 +280,46 @@
 				b.getString(0, b.size(), false));
 	}
 
+	@Test
+	public void testShiftUp() throws Exception {
+		ApplyResult result = init("ShiftUp");
+		assertEquals(1, result.getUpdatedFiles().size());
+		assertEquals(new File(db.getWorkTree(), "ShiftUp"),
+				result.getUpdatedFiles().get(0));
+		checkFile(new File(db.getWorkTree(), "ShiftUp"),
+				b.getString(0, b.size(), false));
+	}
+
+	@Test
+	public void testShiftUp2() throws Exception {
+		ApplyResult result = init("ShiftUp2");
+		assertEquals(1, result.getUpdatedFiles().size());
+		assertEquals(new File(db.getWorkTree(), "ShiftUp2"),
+				result.getUpdatedFiles().get(0));
+		checkFile(new File(db.getWorkTree(), "ShiftUp2"),
+				b.getString(0, b.size(), false));
+	}
+
+	@Test
+	public void testShiftDown() throws Exception {
+		ApplyResult result = init("ShiftDown");
+		assertEquals(1, result.getUpdatedFiles().size());
+		assertEquals(new File(db.getWorkTree(), "ShiftDown"),
+				result.getUpdatedFiles().get(0));
+		checkFile(new File(db.getWorkTree(), "ShiftDown"),
+				b.getString(0, b.size(), false));
+	}
+
+	@Test
+	public void testShiftDown2() throws Exception {
+		ApplyResult result = init("ShiftDown2");
+		assertEquals(1, result.getUpdatedFiles().size());
+		assertEquals(new File(db.getWorkTree(), "ShiftDown2"),
+				result.getUpdatedFiles().get(0));
+		checkFile(new File(db.getWorkTree(), "ShiftDown2"),
+				b.getString(0, b.size(), false));
+	}
+
 	private static byte[] readFile(String patchFile) throws IOException {
 		final InputStream in = getTestResource(patchFile);
 		if (in == null) {
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 0d4b3da..e228e82 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, 2012, IBM Corporation and others. and others
+ * Copyright (C) 2011, 2020 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,18 +9,15 @@
  */
 package org.eclipse.jgit.api;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -168,71 +165,156 @@
 		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;
+		int lastHunkNewLine = -1;
 		for (HunkHeader hh : fh.getHunks()) {
 
+			// We assume hunks to be ordered
+			if (hh.getNewStartLine() <= lastHunkNewLine) {
+				throw new PatchApplyException(MessageFormat
+						.format(JGitText.get().patchApplyException, hh));
+			}
+			lastHunkNewLine = hh.getNewStartLine();
+
 			byte[] b = new byte[hh.getEndOffset() - hh.getStartOffset()];
 			System.arraycopy(hh.getBuffer(), hh.getStartOffset(), b, 0,
 					b.length);
 			RawText hrt = new RawText(b);
 
 			List<String> hunkLines = new ArrayList<>(hrt.size());
-			for (int i = 0; i < hrt.size(); i++)
+			for (int i = 0; i < hrt.size(); i++) {
 				hunkLines.add(hrt.getString(i));
-			int pos = 0;
-			for (int j = 1; j < hunkLines.size(); j++) {
+			}
+
+			if (hh.getNewStartLine() == 0) {
+				// Must be the single hunk for clearing all content
+				if (fh.getHunks().size() == 1
+						&& canApplyAt(hunkLines, newLines, 0)) {
+					newLines.clear();
+					break;
+				}
+				throw new PatchApplyException(MessageFormat
+						.format(JGitText.get().patchApplyException, hh));
+			}
+			// Hunk lines as reported by the hunk may be off, so don't rely on
+			// them.
+			int applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
+			// But they definitely should not go backwards.
+			if (applyAt < afterLastHunk && lineNumberShift < 0) {
+				applyAt = hh.getNewStartLine() - 1;
+				lineNumberShift = 0;
+			}
+			if (applyAt < afterLastHunk) {
+				throw new PatchApplyException(MessageFormat
+						.format(JGitText.get().patchApplyException, hh));
+			}
+			boolean applies = false;
+			int oldLinesInHunk = hh.getLinesContext()
+					+ hh.getOldImage().getLinesDeleted();
+			if (oldLinesInHunk <= 1) {
+				// Don't shift hunks without context lines. Just try the
+				// position corrected by the current lineNumberShift, and if
+				// that fails, the position recorded in the hunk header.
+				applies = canApplyAt(hunkLines, newLines, applyAt);
+				if (!applies && lineNumberShift != 0) {
+					applyAt = hh.getNewStartLine() - 1;
+					applies = applyAt >= afterLastHunk
+							&& canApplyAt(hunkLines, newLines, applyAt);
+				}
+			} else {
+				int maxShift = applyAt - afterLastHunk;
+				for (int shift = 0; shift <= maxShift; shift++) {
+					if (canApplyAt(hunkLines, newLines, applyAt - shift)) {
+						applies = true;
+						applyAt -= shift;
+						break;
+					}
+				}
+				if (!applies) {
+					// Try shifting the hunk downwards
+					applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
+					maxShift = newLines.size() - applyAt - oldLinesInHunk;
+					for (int shift = 1; shift <= maxShift; shift++) {
+						if (canApplyAt(hunkLines, newLines, applyAt + shift)) {
+							applies = true;
+							applyAt += shift;
+							break;
+						}
+					}
+				}
+			}
+			if (!applies) {
+				throw new PatchApplyException(MessageFormat
+						.format(JGitText.get().patchApplyException, hh));
+			}
+			// Hunk applies at applyAt. Apply it, and update afterLastHunk and
+			// lineNumberShift
+			lineNumberShift = applyAt - hh.getNewStartLine() + 1;
+			int sz = hunkLines.size();
+			for (int j = 1; j < sz; j++) {
 				String hunkLine = hunkLines.get(j);
 				switch (hunkLine.charAt(0)) {
 				case ' ':
-					if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
-							hunkLine.substring(1))) {
-						throw new PatchApplyException(MessageFormat.format(
-								JGitText.get().patchApplyException, hh));
-					}
-					pos++;
+					applyAt++;
 					break;
 				case '-':
-					if (hh.getNewStartLine() == 0) {
-						newLines.clear();
-					} else {
-						if (!newLines.get(hh.getNewStartLine() - 1 + pos)
-								.equals(hunkLine.substring(1))) {
-							throw new PatchApplyException(MessageFormat.format(
-									JGitText.get().patchApplyException, hh));
-						}
-						newLines.remove(hh.getNewStartLine() - 1 + pos);
-					}
+					newLines.remove(applyAt);
 					break;
 				case '+':
-					newLines.add(hh.getNewStartLine() - 1 + pos,
-							hunkLine.substring(1));
-					pos++;
+					newLines.add(applyAt++, hunkLine.substring(1));
+					break;
+				default:
 					break;
 				}
 			}
+			afterLastHunk = applyAt;
 		}
-		if (!isNoNewlineAtEndOfFile(fh))
+		if (!isNoNewlineAtEndOfFile(fh)) {
 			newLines.add(""); //$NON-NLS-1$
-		if (!rt.isMissingNewlineAtEnd())
+		}
+		if (!rt.isMissingNewlineAtEnd()) {
 			oldLines.add(""); //$NON-NLS-1$
-		if (!isChanged(oldLines, newLines))
-			return; // don't touch the file
-		StringBuilder sb = new StringBuilder();
-		for (String l : newLines) {
-			// don't bother handling line endings - if it was windows, the \r is
-			// still there!
-			sb.append(l).append('\n');
 		}
-		if (sb.length() > 0) {
-			sb.deleteCharAt(sb.length() - 1);
+		if (!isChanged(oldLines, newLines)) {
+			return; // Don't touch the file
 		}
-		try (Writer fw = new OutputStreamWriter(new FileOutputStream(f),
-				UTF_8)) {
-			fw.write(sb.toString());
+		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');
+				}
+			}
 		}
-
 		getRepository().getFS().setExecute(f, fh.getNewMode() == FileMode.EXECUTABLE_FILE);
 	}
 
+	private boolean canApplyAt(List<String> hunkLines, List<String> newLines,
+			int line) {
+		int sz = hunkLines.size();
+		int limit = newLines.size();
+		int pos = line;
+		for (int j = 1; j < sz; j++) {
+			String hunkLine = hunkLines.get(j);
+			switch (hunkLine.charAt(0)) {
+			case ' ':
+			case '-':
+				if (pos >= limit
+						|| !newLines.get(pos).equals(hunkLine.substring(1))) {
+					return false;
+				}
+				pos++;
+				break;
+			default:
+				break;
+			}
+		}
+		return true;
+	}
+
 	private static boolean isChanged(List<String> ol, List<String> nl) {
 		if (ol.size() != nl.size())
 			return true;