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>tags/v5.8.0.202006091008-r
@@ -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 { |
@@ -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"); | |||
+ } | |||
+ | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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 { |
@@ -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"); | |||
+ } | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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) { |
@@ -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<ApplyResult> { | |||
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; |