blob: 5aea6ceec34f3cd24b4afbf783339c47b5b61766 [file] [log] [blame]
package org.eclipse.compare.patch;
import java.io.*;
import java.text.*;
import java.util.*;
import org.eclipse.jface.util.Assert;
import org.eclipse.core.runtime.*;
/**
* A Patcher
* - knows how to parse various patch file formats into some in-memory structure,
* - holds onto the parsed data and the options to use when applying the patches,
* - knows how to apply the patches to files and folders.
*/
public class Patcher {
private static final String DEV_NULL= "/dev/null";
// diff formats
private static final int CONTEXT= 0;
private static final int ED= 1;
private static final int NORMAL= 2;
private static final int UNIFIED= 3;
// we recognize the following date/time formats
private static DateFormat[] DATE_FORMATS= new DateFormat[] {
new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy"),
new SimpleDateFormat("yyyy/MM/dd kk:mm:ss"),
new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy", Locale.US)
};
private String fName;
private Diff[] fDiffs;
// patch options
private int fStripPrefixSegments;
private int fFuzz;
private boolean fIgnoreWhitespace;
Patcher() {
}
//---- options
void setName(String name) {
fName= name;
}
String getName() {
return fName;
}
/**
* Returns an array of Diffs after a sucessfull call to <code>parse</code>.
* If <code>parse</code> hasn't been called returns <code>null</code>.
*/
Diff[] getDiffs() {
return fDiffs;
}
IPath getPath(Diff diff) {
IPath path= diff.getPath();
if (fStripPrefixSegments > 0 && fStripPrefixSegments < path.segmentCount())
path= path.removeFirstSegments(fStripPrefixSegments);
return path;
}
/**
* Returns <code>true</code> if new value differs from old.
*/
boolean setStripPrefixSegments(int strip) {
if (strip != fStripPrefixSegments) {
fStripPrefixSegments= strip;
return true;
}
return false;
}
/**
* Returns <code>true</code> if new value differs from old.
*/
boolean setFuzz(int fuzz) {
if (fuzz != fFuzz) {
fFuzz= fuzz;
return true;
}
return false;
}
/**
* Returns <code>true</code> if new value differs from old.
*/
boolean setIgnoreWhitespace(boolean ignoreWhitespace) {
if (ignoreWhitespace != fIgnoreWhitespace) {
fIgnoreWhitespace= ignoreWhitespace;
return true;
}
return false;
}
//---- parsing patch files
/* package */ void parse(BufferedReader reader) throws IOException {
List diffs= new ArrayList();
String line= null;
boolean reread= false;
String diffArgs= null;
String fileName= null;
LineReader lr= new LineReader(reader);
// read leading garbage
while (true) {
if (!reread)
line= lr.readLine();
reread= false;
if (line == null)
break;
if (line.length() < 4)
continue; // too short
// remember some infos
if (line.startsWith("Index: ")) {
fileName= line.substring(7).trim();
continue;
}
if (line.startsWith("diff")) {
diffArgs= line.substring(4).trim();
continue;
}
if (line.startsWith("--- ")) {
line= readUnifiedDiff(diffs, lr, line, diffArgs, fileName);
diffArgs= fileName= null;
reread= true;
} else if (line.startsWith("*** ")) {
line= readContextDiff(diffs, lr, line, diffArgs, fileName);
diffArgs= fileName= null;
reread= true;
}
}
lr.close();
fDiffs= (Diff[]) diffs.toArray((Diff[]) new Diff[diffs.size()]);
}
/**
* Returns the next line that does not belong to this diff
*/
private String readUnifiedDiff(List diffs, LineReader reader, String line, String args, String fileName) throws IOException {
String[] oldArgs= split(line.substring(4));
// read info about new file
line= reader.readLine();
if (line == null || !line.startsWith("+++ "))
return line;
String[] newArgs= split(line.substring(4));
Diff diff= new Diff(extractPath(oldArgs, 0, fileName), extractDate(oldArgs, 1),
extractPath(newArgs, 0, fileName), extractDate(newArgs, 1));
diffs.add(diff);
int[] oldRange= new int[2];
int[] newRange= new int[2];
List lines= new ArrayList();
try {
// read lines of hunk
while (true) {
line= reader.readLine();
if (line == null)
return null;
if (line.length() > 0) {
char c= line.charAt(0);
switch (c) {
case '@':
if (line.startsWith("@@ ")) {
// flush old hunk
if (lines.size() > 0) {
new Hunk(diff, oldRange, newRange, lines);
lines.clear();
}
// format: @@ -oldStart,oldLength +newStart,newLength @@
extractPair(line, '-', oldRange);
extractPair(line, '+', newRange);
continue;
}
break;
case ' ':
case '+':
case '-':
lines.add(line);
continue;
case '\\':
if (line.startsWith("No newline at end of file", 2)) {
int lastIndex= lines.size();
if (lastIndex > 0) {
line= (String) lines.get(lastIndex-1);
int end= line.length()-1;
char lc= line.charAt(end);
if (lc == '\n') {
end--;
if (end > 0 && line.charAt(end-1) == '\r')
end--;
} else if (lc == '\r') {
end--;
}
line= line.substring(0, end);
lines.set(lastIndex-1, line);
}
continue;
}
break;
default:
break;
}
}
return line;
}
} finally {
if (lines.size() > 0)
new Hunk(diff, oldRange, newRange, lines);
diff.finish();
}
}
/**
* Returns the next line that does not belong to this diff
*/
private String readContextDiff(List diffs, LineReader reader, String line, String args, String fileName) throws IOException {
String[] oldArgs= split(line.substring(4));
// read info about new file
line= reader.readLine();
if (line == null || !line.startsWith("--- "))
return line;
String[] newArgs= split(line.substring(4));
Diff diff= new Diff(extractPath(oldArgs, 0, fileName), extractDate(oldArgs, 1),
extractPath(newArgs, 0, fileName), extractDate(newArgs, 1));
diffs.add(diff);
int[] oldRange= new int[2];
int[] newRange= new int[2];
List oldLines= new ArrayList();
List newLines= new ArrayList();
List lines= oldLines;
try {
// read lines of hunk
while (true) {
line= reader.readLine();
if (line == null)
return line;
int l= line.length();
if (l == 0)
continue;
if (l > 1) {
switch (line.charAt(0)) {
case '*':
if (line.startsWith("***************")) { // new hunk
// flush old hunk
if (oldLines.size() > 0 || newLines.size() > 0) {
new Hunk(diff, oldRange, newRange, unifyLines(oldLines, newLines));
oldLines.clear();
newLines.clear();
}
continue;
}
if (line.startsWith("*** ")) { // old range
// format: *** oldStart,oldEnd ***
extractPair(line, ' ', oldRange);
oldRange[1]= oldRange[1]-oldRange[0]+1;
lines= oldLines;
continue;
}
break;
case ' ': // context line
case '+': // addition
case '!': // change
if (line.charAt(1) == ' ') {
lines.add(line);
continue;
}
break;
case '-':
if (line.charAt(1) == ' ') { // deletion
lines.add(line);
continue;
}
if (line.startsWith("--- ")) { // new range
// format: *** newStart,newEnd ***
extractPair(line, ' ', newRange);
newRange[1]= newRange[1]-newRange[0]+1;
lines= newLines;
continue;
}
break;
default:
break;
}
}
return line;
}
} finally {
// flush last hunk
if (oldLines.size() > 0 || newLines.size() > 0)
new Hunk(diff, oldRange, newRange, unifyLines(oldLines, newLines));
diff.finish();
}
}
/**
* Creates a List of lines in the unified format from
* two Lists of lines in the 'classic' format.
*/
private List unifyLines(List oldLines, List newLines) {
List result= new ArrayList();
String[] ol= (String[]) oldLines.toArray(new String[oldLines.size()]);
String[] nl= (String[]) newLines.toArray(new String[newLines.size()]);
int oi= 0, ni= 0;
while (true) {
char oc= 0;
String o= null;
if (oi < ol.length) {
o= ol[oi];
oc= o.charAt(0);
}
char nc= 0;
String n= null;
if (ni < nl.length) {
n= nl[ni];
nc= n.charAt(0);
}
// EOF
if (oc == 0 && nc == 0)
break;
// deletion in old
if (oc == '-') {
do {
result.add('-' + o.substring(2));
oi++;
if (oi >= ol.length)
break;
o= ol[oi];
} while (o.charAt(0) == '-');
continue;
}
// addition in new
if (nc == '+') {
do {
result.add('+' + n.substring(2));
ni++;
if (ni >= nl.length)
break;
n= nl[ni];
} while (n.charAt(0) == '+');
continue;
}
// differing lines on both sides
if (oc == '!' && nc == '!') {
// remove old
do {
result.add('-' + o.substring(2));
oi++;
if (oi >= ol.length)
break;
o= ol[oi];
} while (o.charAt(0) == '!');
// add new
do {
result.add('+' + n.substring(2));
ni++;
if (ni >= nl.length)
break;
n= nl[ni];
} while (n.charAt(0) == '!');
continue;
}
// context lines
if (oc == ' ' && nc == ' ') {
do {
Assert.isTrue(o.equals(n), "non matching context lines");
result.add(' ' + o.substring(2));
oi++;
ni++;
if (oi >= ol.length || ni >= nl.length)
break;
o= ol[oi];
n= nl[ni];
} while (o.charAt(0) == ' ' && n.charAt(0) == ' ');
continue;
}
if (oc == ' ') {
do {
result.add(' ' + o.substring(2));
oi++;
if (oi >= ol.length)
break;
o= ol[oi];
} while (o.charAt(0) == ' ');
continue;
}
if (nc == ' ') {
do {
result.add(' ' + n.substring(2));
ni++;
if (ni >= nl.length)
break;
n= nl[ni];
} while (n.charAt(0) == ' ');
continue;
}
Assert.isTrue(false, "unexpected char <" + oc + "> <" + nc + ">");
}
return result;
}
/**
* Breaks the given string into tab separated substrings.
* Leading and trailing whitespace is removed from each token.
*/
private String[] split(String line) {
List l= new ArrayList();
StringTokenizer st= new StringTokenizer(line, "\t");
while (st.hasMoreElements()) {
String token= st.nextToken().trim();
if (token.length() > 0)
l.add(token);
}
return (String[]) l.toArray(new String[l.size()]);
}
/**
* @return the parsed time/date in milliseconds or -1 on error
*/
private long extractDate(String[] args, int n) {
if (n < args.length) {
String line= args[n];
for (int i= 0; i < DATE_FORMATS.length; i++) {
DATE_FORMATS[i].setLenient(true);
try {
Date date= DATE_FORMATS[i].parse(line);
return date.getTime();
} catch (ParseException ex) {
}
}
// System.err.println("can't parse date: <" + line + ">");
}
return -1;
}
/**
* Returns null if file name is "/dev/null".
*/
private IPath extractPath(String[] args, int n, String path2) {
if (n < args.length) {
String path= args[n];
if (DEV_NULL.equals(path))
return null;
int pos= path.lastIndexOf(':');
if (pos >= 0)
path= path.substring(0, pos);
if (path2 != null && !path2.equals(path)) {
// System.out.println("path mismatch: " + path2);
path= path2;
}
return new Path(path);
}
return null;
}
/**
* Tries to extract two integers separated by a comma.
* The parsing of the line starts at the position after
* the first occurrence of the given character start an ends
* at the first blank (or the end of the line).
* If only a single number is found this is assumed to be the length of the range.
* In this case the start of the range is set to 1.
* If an error occurs the range -1,-1 is returned.
*/
private void extractPair(String line, char start, int[] pair) {
pair[0]= pair[1]= -1;
int startPos= line.indexOf(start);
if (startPos < 0) {
System.out.println("parsing error in extractPair: couldn't find \'" + start + "\'");
return;
}
line= line.substring(startPos+1);
int endPos= line.indexOf(' ');
if (endPos < 0) {
System.out.println("parsing error in extractPair: couldn't find end blank");
return;
}
line= line.substring(0, endPos);
int comma= line.indexOf(',');
if (comma >= 0) {
pair[0]= Integer.parseInt(line.substring(0, comma));
pair[1]= Integer.parseInt(line.substring(comma+1));
} else {
pair[0]= 1;
pair[1]= Integer.parseInt(line.substring(comma+1));
}
}
//---- applying a patch file
/**
* Tries to patch the given lines with the specified Diff.
* Any hunk that couldn't be applied is returned in the list failedHunks.
*/
/* package */ void patch(Diff diff, List lines, List failedHunks) {
int shift= 0;
Iterator iter= diff.fHunks.iterator();
while (iter.hasNext()) {
Hunk hunk= (Hunk) iter.next();
shift= patch(hunk, lines, shift, failedHunks);
}
}
/**
* Tries to patch the contents of the given reader with the specified Diff.
* Any hunk that couldn't be applied is returned in the list failedHunks.
*/
/* package */ String patch(Diff diff, BufferedReader reader, List failedHunks) {
List lines= new LineReader(reader).readLines();
if (lines == null)
lines= new ArrayList();
patch(diff, lines, failedHunks);
StringBuffer sb= new StringBuffer();
Iterator iter= lines.iterator();
while (iter.hasNext())
sb.append((String)iter.next());
return sb.toString();
}
/**
* Tries to apply the specified hunk to the given lines.
* If the hunk cannot be applied at the original position
* the methods tries Fuzz lines before and after.
* If this fails the Hunk is added to the given list of failed hunks.
*/
private int patch(Hunk hunk, List lines, int shift, List failedHunks) {
if (tryPatch(hunk, lines, shift)) {
shift+= doPatch(hunk, lines, shift);
} else {
boolean found= false;
int oldShift= shift;
for (int i= shift-1; i > shift-fFuzz; i--) {
if (tryPatch(hunk, lines, i)) {
shift= i;
found= true;
break;
}
}
if (! found) {
for (int i= shift+1; i < shift+fFuzz; i++) {
if (tryPatch(hunk, lines, i)) {
shift= i;
found= true;
break;
}
}
}
if (found) {
//System.out.println("patched hunk at offset: " + (shift-oldShift));
shift+= doPatch(hunk, lines, shift);
} else {
if (failedHunks != null)
failedHunks.add(hunk);
// System.out.println("hunk ignored");
}
}
return shift;
}
private boolean tryPatch(Hunk hunk, List lines, int shift) {
int pos= hunk.fOldStart + shift;
int contextMatches= 0;
int deleteMatches= 0;
for (int i= 0; i < hunk.fLines.length; i++) {
String s= hunk.fLines[i];
Assert.isTrue(s.length() > 0);
String line= s.substring(1);
switch (s.charAt(0)) {
case ' ': // context lines
while (true) {
if (pos < 0 || pos >= lines.size())
return false;
if (linesMatch(line, (String) lines.get(pos))) {
contextMatches++;
pos++;
break;
}
if (contextMatches <= 0)
return false;
pos++;
}
break;
case '-': // deleted lines
while (true) {
if (pos < 0 || pos >= lines.size())
return false;
if (linesMatch(line, (String) lines.get(pos))) {
deleteMatches++;
pos++;
break;
}
if (deleteMatches <= 0)
return false;
pos++;
}
break;
case '+': // added lines
break;
}
}
return true;
}
private int doPatch(Hunk hunk, List lines, int shift) {
int pos= hunk.fOldStart + shift;
for (int i= 0; i < hunk.fLines.length; i++) {
String s= hunk.fLines[i];
Assert.isTrue(s.length() > 0);
String line= s.substring(1);
char type= s.charAt(0);
switch (type) {
case ' ': // context lines
while (true) {
Assert.isTrue(pos < lines.size(), "3");
if (linesMatch(line, (String) lines.get(pos))) {
pos++;
break;
}
pos++;
}
break;
case '-': // deleted lines
while (true) {
Assert.isTrue(pos < lines.size(), "3");
if (linesMatch(line, (String) lines.get(pos))) {
break;
}
pos++;
}
lines.remove(pos);
break;
case '+': // added lines
lines.add(pos, line);
pos++;
break;
}
}
return hunk.fNewLength - hunk.fOldLength;
}
private static 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();
}
/**
* Compares two strings.
* If fIgnoreWhitespace is true whitespace and line endings are ignored.
*/
private boolean linesMatch(String line1, String line2) {
if (fIgnoreWhitespace)
return stripWhiteSpace(line1).equals(stripWhiteSpace(line2));
return line1.equals(line2);
}
/**
* Returns the length (exluding end-of-line characters) of the given string.
*/
/* package */ static int length(String s) {
int l= s.length();
if (l > 0 && s.charAt(l-1) == '\n')
l--;
if (l > 1 && s.charAt(l-2) == '\r')
l--;
return l;
}
}