blob: a9021afd43f3fc60323925ea5616f5f953aff92c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2018 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.BufferedReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.*;
import java.util.regex.Pattern;
import org.eclipse.compare.patch.IFilePatch2;
import org.eclipse.core.runtime.*;
import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.SimpleDateFormat;
public class PatchReader {
private static final boolean DEBUG= false;
private static final String DEV_NULL= "/dev/null"; //$NON-NLS-1$
protected static final String MARKER_TYPE= "org.eclipse.compare.rejectedPatchMarker"; //$NON-NLS-1$
// 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 DateFormat[] fDateFormats= new DateFormat[] {
new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy"), //$NON-NLS-1$
new SimpleDateFormat("yyyy/MM/dd kk:mm:ss"), //$NON-NLS-1$
new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy", Locale.US) //$NON-NLS-1$
};
private boolean fIsWorkspacePatch;
private boolean fIsGitPatch;
private DiffProject[] fDiffProjects;
private FilePatch2[] fDiffs;
// API for writing new multi-project patch format
public static final String MULTIPROJECTPATCH_HEADER= "### Eclipse Workspace Patch"; //$NON-NLS-1$
public static final String MULTIPROJECTPATCH_VERSION= "1.0"; //$NON-NLS-1$
public static final String MULTIPROJECTPATCH_PROJECT= "#P"; //$NON-NLS-1$
private static final Pattern GIT_PATCH_PATTERN= Pattern.compile("^diff --git a/.+ b/.+[\r\n]+$"); //$NON-NLS-1$
/**
* Create a patch reader for the default date formats.
*/
public PatchReader() {
// nothing here
}
/**
* Create a patch reader for the given date formats.
*
* @param dateFormats
* Array of <code>DateFormat</code>s to be used when
* extracting dates from the patch.
*/
public PatchReader(DateFormat[] dateFormats) {
this();
this.fDateFormats = dateFormats;
}
public void parse(BufferedReader reader) throws IOException {
List<FilePatch2> diffs= new ArrayList<>();
HashMap<String, DiffProject> diffProjects= new HashMap<>(4);
String line= null;
boolean reread= false;
String diffArgs= null;
String fileName= null;
// no project means this is a single patch,create a placeholder project for now
// which will be replaced by the target selected by the user in the preview pane
String projectName= ""; //$NON-NLS-1$
this.fIsWorkspacePatch= false;
this.fIsGitPatch = false;
LineReader lr= new LineReader(reader);
lr.ignoreSingleCR(); // Don't treat single CRs as line feeds to be consistent with command line patch
// Test for our format
line= lr.readLine();
if (line != null && line.startsWith(PatchReader.MULTIPROJECTPATCH_HEADER)) {
this.fIsWorkspacePatch= true;
} else {
parse(lr, line);
return;
}
// read leading garbage
while (true) {
if (!reread)
line= lr.readLine();
reread= false;
if (line == null)
break;
if (line.length() < 4)
continue; // too short
if (line.startsWith(PatchReader.MULTIPROJECTPATCH_PROJECT)) {
projectName= line.substring(2).trim();
continue;
}
if (line.startsWith("Index: ")) { //$NON-NLS-1$
fileName= line.substring(7).trim();
continue;
}
if (line.startsWith("diff")) { //$NON-NLS-1$
diffArgs= line.substring(4).trim();
continue;
}
if (line.startsWith("--- ")) { //$NON-NLS-1$
// if there is no current project or
// the current project doesn't equal the newly parsed project
// reset the current project to the newly parsed one, create a new DiffProject
// and add it to the array
DiffProject diffProject;
if (!diffProjects.containsKey(projectName)) {
diffProject= new DiffProject(projectName);
diffProjects.put(projectName, diffProject);
} else {
diffProject= diffProjects.get(projectName);
}
line= readUnifiedDiff(diffs, lr, line, diffArgs, fileName, diffProject);
diffArgs= fileName= null;
reread= true;
}
}
lr.close();
this.fDiffProjects= diffProjects.values().toArray(new DiffProject[diffProjects.size()]);
this.fDiffs = diffs.toArray(new FilePatch2[diffs.size()]);
}
protected FilePatch2 createFileDiff(IPath oldPath, long oldDate,
IPath newPath, long newDate) {
return new FilePatch2(oldPath, oldDate, newPath, newDate);
}
private String readUnifiedDiff(List<FilePatch2> diffs, LineReader lr, String line, String diffArgs, String fileName, DiffProject diffProject) throws IOException {
List<FilePatch2> newDiffs= new ArrayList<>();
String nextLine= readUnifiedDiff(newDiffs, lr, line, diffArgs, fileName);
for (Iterator<FilePatch2> iter= newDiffs.iterator(); iter.hasNext();) {
FilePatch2 diff= iter.next();
diffProject.add(diff);
diffs.add(diff);
}
return nextLine;
}
public void parse(LineReader lr, String line) throws IOException {
List<FilePatch2> diffs= new ArrayList<>();
boolean reread= false;
String diffArgs= null;
String fileName= null;
List<String> headerLines = new ArrayList<>();
boolean foundDiff= false;
// read leading garbage
reread= line!=null;
while (true) {
if (!reread)
line= lr.readLine();
reread= false;
if (line == null)
break;
// remember some infos
if (line.startsWith("Index: ")) { //$NON-NLS-1$
fileName= line.substring(7).trim();
} else if (line.startsWith("diff")) { //$NON-NLS-1$
if (!foundDiff && GIT_PATCH_PATTERN.matcher(line).matches())
this.fIsGitPatch= true;
foundDiff= true;
diffArgs= line.substring(4).trim();
} else if (line.startsWith("--- ")) { //$NON-NLS-1$
line= readUnifiedDiff(diffs, lr, line, diffArgs, fileName);
if (!headerLines.isEmpty())
setHeader(diffs.get(diffs.size() - 1), headerLines);
diffArgs= fileName= null;
reread= true;
} else if (line.startsWith("*** ")) { //$NON-NLS-1$
line= readContextDiff(diffs, lr, line, diffArgs, fileName);
if (!headerLines.isEmpty())
setHeader(diffs.get(diffs.size() - 1), headerLines);
diffArgs= fileName= null;
reread= true;
}
// Any lines we read here are header lines.
// However, if reread is set, we will add them to the header on the next pass through
if (!reread) {
headerLines.add(line);
}
}
lr.close();
this.fDiffs = diffs.toArray(new FilePatch2[diffs.size()]);
}
private void setHeader(FilePatch2 diff, List<String> headerLines) {
String header = LineReader.createString(false, headerLines);
diff.setHeader(header);
headerLines.clear();
}
/*
* Returns the next line that does not belong to this diff
*/
protected String readUnifiedDiff(List<FilePatch2> 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("+++ ")) //$NON-NLS-1$
return line;
String[] newArgs= split(line.substring(4));
FilePatch2 diff = createFileDiff(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];
int remainingOld= -1; // remaining old lines for current hunk
int remainingNew= -1; // remaining new lines for current hunk
List<String> lines= new ArrayList<>();
boolean encounteredPlus = false;
boolean encounteredMinus = false;
boolean encounteredSpace = false;
try {
// read lines of hunk
while (true) {
line= reader.readLine();
if (line == null)
return null;
if (reader.lineContentLength(line) == 0) {
//System.out.println("Warning: found empty line in hunk; ignored");
//lines.add(' ' + line);
continue;
}
char c= line.charAt(0);
if (remainingOld == 0 && remainingNew == 0 && c != '@' && c != '\\') {
return line;
}
switch (c) {
case '@':
if (line.startsWith("@@ ")) { //$NON-NLS-1$
// flush old hunk
if (lines.size() > 0) {
Hunk.createHunk(diff, oldRange, newRange, lines, encounteredPlus, encounteredMinus, encounteredSpace);
lines.clear();
}
// format: @@ -oldStart,oldLength +newStart,newLength @@
extractPair(line, '-', oldRange);
extractPair(line, '+', newRange);
remainingOld= oldRange[1];
remainingNew= newRange[1];
continue;
}
break;
case ' ':
encounteredSpace= true;
remainingOld--;
remainingNew--;
lines.add(line);
continue;
case '+':
encounteredPlus= true;
remainingNew--;
lines.add(line);
continue;
case '-':
encounteredMinus= true;
remainingOld--;
lines.add(line);
continue;
case '\\':
if (line.indexOf("newline at end") > 0) { //$NON-NLS-1$
int lastIndex= lines.size();
if (lastIndex > 0) {
line= lines.get(lastIndex - 1);
int end= line.length() - 1;
char lc= line.charAt(end);
if (lc == '\n') {
end--;
if (end > 0 && line.charAt(end) == '\r')
end--;
} else if (lc == '\r') {
end--;
}
line= line.substring(0, end + 1);
lines.set(lastIndex - 1, line);
}
continue;
}
break;
case '#':
break;
case 'I':
if (line.indexOf("Index:") == 0) //$NON-NLS-1$
break;
//$FALL-THROUGH$
case 'd':
if (line.indexOf("diff ") == 0) //$NON-NLS-1$
break;
//$FALL-THROUGH$
case 'B':
if (line.indexOf("Binary files differ") == 0) //$NON-NLS-1$
break;
//$FALL-THROUGH$
default:
break;
}
return line;
}
} finally {
if (lines.size() > 0)
Hunk.createHunk(diff, oldRange, newRange, lines, encounteredPlus, encounteredMinus, encounteredSpace);
}
}
/*
* Returns the next line that does not belong to this diff
*/
private String readContextDiff(List<FilePatch2> 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("--- ")) //$NON-NLS-1$
return line;
String[] newArgs= split(line.substring(4));
FilePatch2 diff = createFileDiff(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<String> oldLines= new ArrayList<>();
List<String> newLines= new ArrayList<>();
List<String> lines= oldLines;
boolean encounteredPlus = false;
boolean encounteredMinus = false;
boolean encounteredSpace = false;
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 //$NON-NLS-1$
// flush old hunk
if (oldLines.size() > 0 || newLines.size() > 0) {
Hunk.createHunk(diff, oldRange, newRange, unifyLines(oldLines, newLines), encounteredPlus, encounteredMinus, encounteredSpace);
oldLines.clear();
newLines.clear();
}
continue;
}
if (line.startsWith("*** ")) { // old range //$NON-NLS-1$
// format: *** oldStart,oldEnd ***
extractPair(line, ' ', oldRange);
if (oldRange[0] == 0) {
oldRange[1] = 0; // In case of the file addition
} else {
oldRange[1] = oldRange[1] - oldRange[0] + 1;
}
lines= oldLines;
continue;
}
break;
case ' ': // context line
if (line.charAt(1) == ' ') {
lines.add(line);
continue;
}
break;
case '+': // addition
if (line.charAt(1) == ' ') {
encounteredPlus = true;
lines.add(line);
continue;
}
break;
case '!': // change
if (line.charAt(1) == ' ') {
encounteredSpace = true;
lines.add(line);
continue;
}
break;
case '-':
if (line.charAt(1) == ' ') { // deletion
encounteredMinus = true;
lines.add(line);
continue;
}
if (line.startsWith("--- ")) { // new range //$NON-NLS-1$
// format: *** newStart,newEnd ***
extractPair(line, ' ', newRange);
if (newRange[0] == 0) {
newRange[1] = 0; // In case of the file removal
} else {
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)
Hunk.createHunk(diff, oldRange, newRange, unifyLines(oldLines, newLines), encounteredPlus, encounteredMinus, encounteredSpace);
}
}
/*
* Creates a List of lines in the unified format from
* two Lists of lines in the 'classic' format.
*/
private List<String> unifyLines(List<String> oldLines, List<String> newLines) {
List<String> result= new ArrayList<>();
String[] ol= oldLines.toArray(new String[oldLines.size()]);
String[] nl= 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"); //$NON-NLS-1$
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 + ">"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
return result;
}
/*
* @return the parsed time/date in milliseconds or IFilePatch.DATE_UNKNOWN
* (0) on error
*/
private long extractDate(String[] args, int n) {
if (n < args.length) {
String line= args[n];
for (int i= 0; i < this.fDateFormats.length; i++) {
this.fDateFormats[i].setLenient(true);
try {
Date date= this.fDateFormats[i].parse(line);
return date.getTime();
} catch (ParseException ex) {
// silently ignored
}
}
// System.err.println("can't parse date: <" + line + ">");
}
return IFilePatch2.DATE_UNKNOWN;
}
/*
* 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)) {
if (DEBUG) System.out.println("path mismatch: " + path2); //$NON-NLS-1$
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 start of a one line range.
* 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) {
if (DEBUG) System.out.println("parsing error in extractPair: couldn't find \'" + start + "\'"); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
line= line.substring(startPos+1);
int endPos= line.indexOf(' ');
if (endPos < 0) {
if (DEBUG) System.out.println("parsing error in extractPair: couldn't find end blank"); //$NON-NLS-1$
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 { // abbreviated form for one line patch
pair[0]= Integer.parseInt(line);
pair[1]= 1;
}
}
/*
* Breaks the given string into tab separated substrings.
* Leading and trailing whitespace is removed from each token.
*/
private String[] split(String line) {
List<String> l= new ArrayList<>();
StringTokenizer st= new StringTokenizer(line, "\t"); //$NON-NLS-1$
while (st.hasMoreElements()) {
String token= st.nextToken().trim();
if (token.length() > 0)
l.add(token);
}
return l.toArray(new String[l.size()]);
}
public boolean isWorkspacePatch() {
return this.fIsWorkspacePatch;
}
public boolean isGitPatch() {
return this.fIsGitPatch;
}
public DiffProject[] getDiffProjects() {
return this.fDiffProjects;
}
public FilePatch2[] getDiffs() {
return this.fDiffs;
}
public FilePatch2[] getAdjustedDiffs() {
if (!isWorkspacePatch() || this.fDiffs.length == 0)
return this.fDiffs;
List<FilePatch2> result = new ArrayList<>();
for (int i = 0; i < this.fDiffs.length; i++) {
FilePatch2 diff = this.fDiffs[i];
result.add(diff.asRelativeDiff());
}
return result.toArray(new FilePatch2[result.size()]);
}
}