blob: 033cf8e26dce4d1f4e695df212b5fc7c50d7c145 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2011 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.compare.internal.core.patch;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.compare.patch.IFilePatchResult;
import org.eclipse.compare.patch.IHunk;
import org.eclipse.compare.patch.PatchConfiguration;
import org.eclipse.core.runtime.Assert;
/**
* A Hunk describes a range of changed lines and some context lines.
*/
public class Hunk implements IHunk {
private FilePatch2 fParent;
private int fOldStart, fOldLength;
private int fNewStart, fNewLength;
private String[] fLines;
private int hunkType;
private String charset = null;
public static Hunk createHunk(FilePatch2 parent, int[] oldRange, int[] newRange,
List<String> lines, boolean hasLineAdditions, boolean hasLineDeletions, boolean hasContextLines) {
int oldStart = 0;
int oldLength = 0;
int newStart = 0;
int newLength = 0;
if (oldRange[0] > 0)
oldStart= oldRange[0]-1; // line number start at 0!
else
oldStart= 0;
oldLength= oldRange[1];
if (newRange[0] > 0)
newStart= newRange[0]-1; // line number start at 0!
else
newStart= 0;
newLength= newRange[1];
int hunkType = FilePatch2.CHANGE;
if (!hasContextLines) {
if (hasLineAdditions && !hasLineDeletions) {
hunkType = FilePatch2.ADDITION;
} else if (!hasLineAdditions && hasLineDeletions) {
hunkType = FilePatch2.DELETION;
}
}
return new Hunk(parent, hunkType, oldStart, oldLength, newStart, newLength, lines.toArray(new String[lines.size()]));
}
public Hunk(FilePatch2 parent, int hunkType, int oldStart, int oldLength,
int newStart, int newLength, String[] lines) {
this.fParent = parent;
if (this.fParent != null) {
this.fParent.add(this);
}
this.hunkType = hunkType;
this.fOldLength = oldLength;
this.fOldStart = oldStart;
this.fNewLength = newLength;
this.fNewStart = newStart;
this.fLines = lines;
}
public Hunk(FilePatch2 parent, Hunk toCopy) {
this(parent, toCopy.hunkType, toCopy.fOldStart, toCopy.fOldLength, toCopy.fNewStart, toCopy.fNewLength, toCopy.fLines);
}
/*
* Returns the contents of this hunk.
* Each line starts with a control character. Their meaning is as follows:
* <ul>
* <li>
* '+': add the line
* <li>
* '-': delete the line
* <li>
* ' ': no change, context line
* </ul>
*/
public String getContent() {
StringBuffer sb= new StringBuffer();
for (int i= 0; i < this.fLines.length; i++) {
String line= this.fLines[i];
sb.append(line.substring(0, LineReader.length(line)));
sb.append('\n');
}
return sb.toString();
}
/*
* Returns a descriptive String for this hunk.
* It is in the form old_start,old_length -> new_start,new_length.
*/
String getDescription() {
StringBuffer sb= new StringBuffer();
sb.append(Integer.toString(this.fOldStart));
sb.append(',');
sb.append(Integer.toString(this.fOldLength));
sb.append(" -> "); //$NON-NLS-1$
sb.append(Integer.toString(this.fNewStart));
sb.append(',');
sb.append(Integer.toString(this.fNewLength));
return sb.toString();
}
public String getRejectedDescription() {
StringBuffer sb= new StringBuffer();
sb.append("@@ -"); //$NON-NLS-1$
sb.append(Integer.toString(this.fOldStart));
sb.append(',');
sb.append(Integer.toString(this.fOldLength));
sb.append(" +"); //$NON-NLS-1$
sb.append(Integer.toString(this.fNewStart));
sb.append(',');
sb.append(Integer.toString(this.fNewLength));
sb.append(" @@"); //$NON-NLS-1$
return sb.toString();
}
public int getHunkType(boolean reverse) {
if (reverse) {
if (this.hunkType == FilePatch2.ADDITION)
return FilePatch2.DELETION;
if (this.hunkType == FilePatch2.DELETION)
return FilePatch2.ADDITION;
}
return this.hunkType;
}
void setHunkType(int hunkType) {
this.hunkType = hunkType;
}
public String[] getLines() {
return this.fLines;
}
@Override
public String[] getUnifiedLines() {
String[] ret = new String[this.fLines.length];
System.arraycopy(this.fLines, 0, ret, 0, this.fLines.length);
return ret;
}
/**
* Set the parent of this hunk. This method
* should only be invoked from {@link FilePatch2#add(Hunk)}
* @param diff the parent of this hunk
*/
void setParent(FilePatch2 diff) {
if (this.fParent == diff)
return;
if (this.fParent != null)
this.fParent.remove(this);
this.fParent = diff;
}
public FilePatch2 getParent() {
return this.fParent;
}
/*
* Tries to apply the given hunk on the specified lines.
* The parameter shift is added to the line numbers given
* in the hunk.
*/
public boolean tryPatch(PatchConfiguration configuration, List<String> lines, int shift, int fuzz) {
boolean reverse = configuration.isReversed();
int pos = getStart(reverse) + shift;
List<String> contextLines = new ArrayList<>();
boolean contextLinesMatched = true;
boolean precedingLinesChecked = false;
for (int i= 0; i < this.fLines.length; i++) {
String s = this.fLines[i];
Assert.isTrue(s.length() > 0);
String line = s.substring(1);
char controlChar = s.charAt(0);
if (controlChar == ' ') { // context lines
if (pos < 0 || pos >= lines.size())
return false;
contextLines.add(line);
if (linesMatch(configuration, line, lines.get(pos))) {
pos++;
continue;
} else if (fuzz > 0) {
// doesn't match, use the fuzz factor
contextLinesMatched = false;
pos++;
continue;
}
return false;
} else if (isDeletedDelimeter(controlChar, reverse)) {
// deleted lines
if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0)
// context lines inside hunk don't match
return false;
// check following context lines if exist
// use the fuzz factor if needed
if (!precedingLinesChecked
&& !contextLinesMatched
&& contextLines.size() >= fuzz
&& !checkPrecedingContextLines(configuration, lines,
fuzz, pos, contextLines))
return false;
// else if there is less or equal context line to the fuzz
// factor we ignore them all and treat as matching
precedingLinesChecked = true;
contextLines.clear();
contextLinesMatched = true;
if (pos < 0 || pos >= lines.size()) // out of the file
return false;
if (linesMatch(configuration, line, lines.get(pos))) {
pos++;
continue; // line matched, continue with the next one
}
// We must remove all lines at once, return false if this
// fails. In other words, all lines considered for deletion
// must be found one by one.
return false;
} else if (isAddedDelimeter(controlChar, reverse)) {
if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0)
return false;
if (!precedingLinesChecked
&& !contextLinesMatched
&& contextLines.size() >= fuzz
&& !checkPrecedingContextLines(configuration, lines,
fuzz, pos, contextLines))
return false;
precedingLinesChecked = true;
contextLines.clear();
contextLinesMatched = true;
// we don't have to do anything more for a 'try'
} else
Assert.isTrue(false, "tryPatch: unknown control character: " + controlChar); //$NON-NLS-1$
}
// check following context lines if exist
if (!contextLinesMatched
&& fuzz > 0
&& contextLines.size() > fuzz
&& !checkFollowingContextLines(configuration, lines, fuzz, pos,
contextLines))
return false;
return true;
}
private boolean checkPrecedingContextLines(
PatchConfiguration configuration, List<String> lines, int fuzz, int pos,
List<String> contextLines) {
// ignore from the beginning
for (int j = fuzz; j < contextLines.size(); j++) {
if (!linesMatch(configuration, contextLines.get(j),
lines.get(pos - contextLines.size() + j)))
return false;
}
return true;
}
private boolean checkFollowingContextLines(
PatchConfiguration configuration, List<String> lines, int fuzz, int pos,
List<String> contextLines) {
if (!contextLines.isEmpty()) {
// ignore from the end
for (int j = 0; j < contextLines.size() - fuzz; j++) {
if (!linesMatch(configuration, contextLines.get(j),
lines.get(pos - contextLines.size() + j)))
return false;
}
}
return true;
}
public int getStart(boolean after) {
if (after) {
return this.fNewStart;
}
return this.fOldStart;
}
public void setStart(int start, boolean after) {
if (after) {
this.fNewStart = start;
} else {
this.fOldStart = start;
}
}
public int getLength(boolean after) {
if (after) {
return this.fNewLength;
}
return this.fOldLength;
}
private int getShift(boolean reverse) {
if (reverse) {
return this.fOldLength - this.fNewLength;
}
return this.fNewLength - this.fOldLength;
}
int doPatch(PatchConfiguration configuration, List<String> lines, int shift, int fuzz) {
boolean reverse = configuration.isReversed();
int pos = getStart(reverse) + shift;
List<String> contextLines = new ArrayList<>();
boolean contextLinesMatched = true;
boolean precedingLinesChecked = false;
String lineDelimiter = getLineDelimiter(lines);
for (int i= 0; i < this.fLines.length; i++) {
String s= this.fLines[i];
Assert.isTrue(s.length() > 0);
String line= s.substring(1);
char controlChar= s.charAt(0);
if (controlChar == ' ') {
// context lines
Assert.isTrue(pos < lines.size(), "doPatch: inconsistency in context"); //$NON-NLS-1$
contextLines.add(line);
if (linesMatch(configuration, line, lines.get(pos))) {
pos++;
continue;
} else if (fuzz > 0) {
// doesn't match, use the fuzz factor
contextLinesMatched = false;
pos++;
continue;
}
Assert.isTrue(false, "doPatch: context doesn't match"); //$NON-NLS-1$
// pos++;
} else if (isDeletedDelimeter(controlChar, reverse)) {
// deleted lines
if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0)
// context lines inside hunk don't match
Assert.isTrue(false, "doPatch: context lines inside hunk don't match"); //$NON-NLS-1$
// check following context lines if exist
// use the fuzz factor if needed
if (!precedingLinesChecked
&& !contextLinesMatched
&& contextLines.size() >= fuzz
&& !checkPrecedingContextLines(configuration, lines,
fuzz, pos, contextLines))
Assert.isTrue(false, "doPatch: preceding context lines don't match, even though fuzz factor has been used"); //$NON-NLS-1$;
// else if there is less or equal context line to the fuzz
// factor we ignore them all and treat as matching
precedingLinesChecked = true;
contextLines.clear();
contextLinesMatched = true;
lines.remove(pos);
} else if (isAddedDelimeter(controlChar, reverse)) {
// added lines
if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0)
Assert.isTrue(false, "doPatch: context lines inside hunk don't match"); //$NON-NLS-1$
if (!precedingLinesChecked
&& !contextLinesMatched
&& contextLines.size() >= fuzz
&& !checkPrecedingContextLines(configuration, lines,
fuzz, pos, contextLines))
Assert.isTrue(false, "doPatch: preceding context lines don't match, even though fuzz factor has been used"); //$NON-NLS-1$;
precedingLinesChecked = true;
contextLines.clear();
contextLinesMatched = true;
// if the line contains a delimiter, use a proper one
if (line.length() > LineReader.length(line))
line = line.substring(0, LineReader.length(line)) + lineDelimiter;
if (getLength(reverse) == 0 && pos+1 < lines.size())
lines.add(pos+1, line);
else
lines.add(pos, line);
pos++;
} else
Assert.isTrue(false, "doPatch: unknown control character: " + controlChar); //$NON-NLS-1$
}
return getShift(reverse);
}
private boolean isDeletedDelimeter(char controlChar, boolean reverse) {
return (!reverse && controlChar == '-') || (reverse && controlChar == '+');
}
private boolean isAddedDelimeter(char controlChar, boolean reverse) {
return (reverse && controlChar == '-') || (!reverse && controlChar == '+');
}
/*
* Compares two strings.
* If fIgnoreWhitespace is true whitespace is ignored.
*/
private boolean linesMatch(PatchConfiguration configuration, String line1, String line2) {
if (configuration.isIgnoreWhitespace())
return stripWhiteSpace(line1).equals(stripWhiteSpace(line2));
if (isIgnoreLineDelimiter()) {
int l1= LineReader.length(line1);
int l2= LineReader.length(line2);
if (l1 != l2)
return false;
return line1.regionMatches(0, line2, 0, l1);
}
return line1.equals(line2);
}
private boolean isIgnoreLineDelimiter() {
return true;
}
private String getLineDelimiter(List<String> lines) {
if (lines.size() > 0) {
// get a line separator from the file being patched
String line0 = lines.get(0);
return line0.substring(LineReader.length(line0));
} else if (this.fLines.length > 0) {
// if the file doesn't exist use a line separator from the patch
return this.fLines[0].substring(LineReader.length(this.fLines[0]));
}
return System.getProperty("line.separator"); //$NON-NLS-1$
}
/*
* Returns the given string with all whitespace characters removed.
* Whitespace is defined by <code>Character.isWhitespace(...)</code>.
*/
private String stripWhiteSpace(String s) {
StringBuffer sb= new StringBuffer();
int l= s.length();
for (int i= 0; i < l; i++) {
char c= s.charAt(i);
if (!Character.isWhitespace(c))
sb.append(c);
}
return sb.toString();
}
public String getContents(boolean isAfterState, boolean reverse) {
StringBuffer result= new StringBuffer();
for (int i= 0; i<this.fLines.length; i++) {
String line= this.fLines[i];
String rest= line.substring(1);
char c = line.charAt(0);
if (c == ' ') {
result.append(rest);
} else if (isDeletedDelimeter(c, reverse) && !isAfterState) {
result.append(rest);
} else if (isAddedDelimeter(c, reverse) && isAfterState) {
result.append(rest);
}
}
return result.toString();
}
@Override
public String getLabel() {
return getDescription();
}
@Override
public int getStartPosition() {
return getStart(false);
}
@Override
public InputStream getOriginalContents() {
String contents = getContents(false, false);
return asInputStream(contents);
}
@Override
public InputStream getPatchedContents() {
String contents = getContents(true, false);
return asInputStream(contents);
}
private InputStream asInputStream(String contents) {
String charSet = getCharset();
return FileDiffResult.asInputStream(contents, charSet);
}
void setCharset(String charset) {
this.charset = charset;
}
/**
* {@inheritDoc}
* @deprecated This method can be called before the first attempt to apply
* the hunk when it is impossible to determine the encoding and
* in this case it always returns null. Please see
* {@link IFilePatchResult#getCharset()} as a proper way to
* obtain charset.
*/
@Deprecated
@Override
public String getCharset() {
return this.charset;
}
}