From ed481f96b811c2c50d58f35200657484320621dc Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Fri, 24 Apr 2020 15:00:01 +0200 Subject: [PATCH] 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 --- .../org/eclipse/jgit/diff/ShiftDown.patch | 14 ++ .../org/eclipse/jgit/diff/ShiftDown2.patch | 24 +++ .../eclipse/jgit/diff/ShiftDown2_PostImage | 75 ++++++++ .../org/eclipse/jgit/diff/ShiftDown2_PreImage | 68 +++++++ .../org/eclipse/jgit/diff/ShiftDown_PostImage | 71 ++++++++ .../org/eclipse/jgit/diff/ShiftDown_PreImage | 68 +++++++ .../org/eclipse/jgit/diff/ShiftUp.patch | 14 ++ .../org/eclipse/jgit/diff/ShiftUp2.patch | 23 +++ .../org/eclipse/jgit/diff/ShiftUp2_PostImage | 38 ++++ .../org/eclipse/jgit/diff/ShiftUp2_PreImage | 32 ++++ .../org/eclipse/jgit/diff/ShiftUp_PostImage | 35 ++++ .../org/eclipse/jgit/diff/ShiftUp_PreImage | 32 ++++ .../eclipse/jgit/api/ApplyCommandTest.java | 42 ++++- .../org/eclipse/jgit/api/ApplyCommand.java | 166 +++++++++++++----- 14 files changed, 659 insertions(+), 43 deletions(-) create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown.patch create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2.patch create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PostImage create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown2_PreImage create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PostImage create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftDown_PreImage create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp.patch create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2.patch create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PostImage create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp2_PreImage create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PostImage create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ShiftUp_PreImage 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 000000000..74c33714b --- /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 000000000..a2b34b354 --- /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 000000000..738484eef --- /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 000000000..e1ee19c4d --- /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 000000000..5c6e9bccb --- /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 000000000..e1ee19c4d --- /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 000000000..aa994a12e --- /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 000000000..eca99714c --- /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 000000000..e279ecedd --- /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 000000000..8b9727b01 --- /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 000000000..25dc192b0 --- /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 000000000..8b9727b01 --- /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 63cd21f59..055eba718 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 @@ public class ApplyCommandTest extends RepositoryTestCase { 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 0d4b3da6a..e228e8276 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 @@ public class ApplyCommand extends GitCommand { for (int i = 0; i < rt.size(); i++) oldLines.add(rt.getString(i)); List 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 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 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 hunkLines, List 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 ol, List nl) { if (ol.size() != nl.size()) return true;