/*
* ====================================================================
* Copyright (c) 2004-2010 TMate Software Ltd. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://svnkit.com/license.html.
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
* ====================================================================
*/
package org.tmatesoft.svn.core.internal.wc.patch;
import java.io.IOException;
import java.util.Comparator;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.internal.wc.patch.SVNPatchFileStream.SVNPatchFileLineFilter;
import org.tmatesoft.svn.core.internal.wc.patch.SVNPatchFileStream.SVNPatchFileLineTransformer;
/**
* A single hunk inside a patch.
*
* @version 1.3
* @author TMate Software Ltd.
*/
public class SVNPatchHunk {
public static class SVNPatchHunkRange {
private int start;
private int length;
public int getStart() {
return start;
}
public int getLength() {
return length;
}
}
/**
* Compare function for sorting hunks after parsing. We sort hunks by their
* original line offset.
*/
public static final Comparator COMPARATOR = new Comparator() {
public int compare(Object a, Object b) {
final SVNPatchHunk ha = (SVNPatchHunk) a;
final SVNPatchHunk hb = (SVNPatchHunk) b;
if (ha.original.start < hb.original.start)
return -1;
if (ha.original.start > hb.original.start)
return 1;
return 0;
};
};
/**
* A stream line-filter which allows only original text from a hunk, and
* filters special lines (which start with a backslash).
*/
private static final SVNPatchFileLineFilter original_line_filter = new SVNPatchFileLineFilter() {
public boolean lineFilter(String line) {
return (getChar(line,0) == '+' || getChar(line,0) == '\\');
}
};
/**
* A stream line-filter which allows only modified text from a hunk, and
* filters special lines (which start with a backslash).
*/
private static final SVNPatchFileLineFilter modified_line_filter = new SVNPatchFileLineFilter() {
public boolean lineFilter(String line) {
return (getChar(line,0) == '-' || getChar(line,0) == '\\');
}
};
/** Line-transformer callback to shave leading diff symbols. */
private static final SVNPatchFileLineTransformer remove_leading_char_transformer = new SVNPatchFileLineTransformer() {
public String lineTransformer(String line) {
if (getChar(line,0) == '+' || getChar(line,0) == '-' || getChar(line,0) == ' ') {
return line.substring(1);
}
return line;
}
};
/**
* The hunk's unidiff text as it appeared in the patch file, without range
* information.
*/
private SVNPatchFileStream diffText;
/**
* The original and modified texts in the hunk range. Derived from the diff
* text.
*
* For example, consider a hunk such as:
*
* <pre>
* @@ -1,5 +1,5 @@
* #include <stdio.h>
* int main(int argc, char *argv[])
* {
* - printf("Hello World!\n");
* + printf("I like Subversion!\n");
* }
* </pre>
*
* Then, the original text described by the hunk is:
*
* <pre>
* #include <stdio.h>
* int main(int argc, char *argv[])
* {
* printf("Hello World!\n");
* }
* </pre>
*
* And the modified text described by the hunk is:
*
* <pre>
* #include <stdio.h>
* int main(int argc, char *argv[])
* {
* printf("I like Subversion!\n");
* }
* </pre>
*
* Because these streams make use of line filtering and transformation, they
* should only be read line-by-line with svn_stream_readline(). Reading them
* with svn_stream_read() will not yield the expected result, because it
* will return the unidiff text from the patch file unmodified. The streams
* support resetting.
*/
private SVNPatchFileStream originalText;
private SVNPatchFileStream modifiedText;
/**
* Hunk ranges as they appeared in the patch file. All numbers are lines,
* not bytes.
*/
private SVNPatchHunkRange original = new SVNPatchHunkRange();
private SVNPatchHunkRange modified = new SVNPatchHunkRange();
/** Number of lines starting with ' ' before first '+' or '-'. */
private long leadingContext;
/** Number of lines starting with ' ' after last '+' or '-'. */
private long trailingContext;
public SVNPatchFileStream getDiffText() {
return diffText;
}
public SVNPatchFileStream getOriginalText() {
return originalText;
}
public SVNPatchFileStream getModifiedText() {
return modifiedText;
}
public SVNPatchHunkRange getOriginal() {
return original;
}
public SVNPatchHunkRange getModified() {
return modified;
}
public long getLeadingContext() {
return leadingContext;
}
public long getTrailingContext() {
return trailingContext;
}
public void close() throws IOException {
if (originalText != null) {
originalText.close();
}
if (modifiedText != null) {
modifiedText.close();
}
if (diffText != null) {
diffText.close();
}
}
/**
* Return the next HUNK from a PATCH, using STREAM to read data from the
* patch file. If no hunk can be found, set HUNK to NULL.
*
* @throws IOException
* @throws SVNException
*/
public static SVNPatchHunk parseNextHunk(SVNPatch patch) throws IOException, SVNException {
boolean eof, in_hunk, hunk_seen;
long pos, last_line;
long start, end;
long original_lines;
long leading_context;
long trailing_context;
boolean changed_line_seen;
if (patch.getPatchFile().isEOF()) {
/* No more hunks here. */
return null;
}
in_hunk = false;
hunk_seen = false;
leading_context = 0;
trailing_context = 0;
changed_line_seen = false;
start = 0;
end = 0;
original_lines = 0;
SVNPatchHunk hunk = new SVNPatchHunk();
/* Get current seek position */
pos = patch.getPatchFile().getSeekPosition();
final StringBuffer lineBuf = new StringBuffer();
do {
/* Remember the current line's offset, and read the line. */
last_line = pos;
lineBuf.setLength(0);
eof = patch.getPatchFile().readLine(lineBuf);
final String line = lineBuf.toString();
if (!eof) {
/* Update line offset for next iteration */
pos = patch.getPatchFile().getSeekPosition();
}
/*
* Lines starting with a backslash are comments, such as "\ No
* newline at end of file".
*/
if (getChar(line, 0) == '\\')
continue;
if (in_hunk) {
char c;
if (!hunk_seen) {
/*
* We're reading the first line of the hunk, so the start of
* the line just read is the hunk text's byte offset.
*/
start = last_line;
}
c = getChar(line, 0);
/* Tolerate chopped leading spaces on empty lines. */
if (original_lines > 0 && (c == ' ' || (!eof && line.length() == 0))) {
hunk_seen = true;
original_lines--;
if (changed_line_seen)
trailing_context++;
else
leading_context++;
} else if (c == '+' || c == '-') {
hunk_seen = true;
changed_line_seen = true;
/*
* A hunk may have context in the middle. We only want the
* last lines of context.
*/
if (trailing_context > 0)
trailing_context = 0;
if (original_lines > 0 && c == '-')
original_lines--;
} else {
in_hunk = false;
/*
* The start of the current line marks the first byte after
* the hunk text.
*/
end = last_line;
break; /* Hunk was empty or has been read. */
}
} else {
if (line.startsWith(SVNPatch.ATAT)) {
/*
* Looks like we have a hunk header, let's try to rip it
* apart.
*/
in_hunk = parseHunkHeader(line, hunk);
if (in_hunk)
original_lines = hunk.original.length;
} else if (line.startsWith(SVNPatch.MINUS))
/* This could be a header of another patch. Bail out. */
break;
}
} while (!eof);
if (!eof) {
/*
* Rewind to the start of the line just read, so subsequent calls to
* this function or svn_diff__parse_next_patch() don't end up
* skipping the line -- it may contain a patch or hunk header.
*/
patch.getPatchFile().setSeekPosition(last_line);
}
if (hunk_seen && start < end) {
/* Create a stream which returns the hunk text itself. */
SVNPatchFileStream diff_text = SVNPatchFileStream.openRangeReadOnly(patch.getPath(), start, end);
/* Create a stream which returns the original hunk text. */
SVNPatchFileStream original_text = SVNPatchFileStream.openRangeReadOnly(patch.getPath(), start, end);
original_text.setLineFilter(original_line_filter);
original_text.setLineTransformer(remove_leading_char_transformer);
/* Create a stream which returns the modified hunk text. */
SVNPatchFileStream modified_text = SVNPatchFileStream.openRangeReadOnly(patch.getPath(), start, end);
modified_text.setLineFilter(modified_line_filter);
modified_text.setLineTransformer(remove_leading_char_transformer);
/* Set the hunk's texts. */
hunk.diffText = diff_text;
hunk.originalText = original_text;
hunk.modifiedText = modified_text;
hunk.leadingContext = leading_context;
hunk.trailingContext = trailing_context;
} else {
/* Something went wrong, just discard the result. */
return null;
}
return hunk;
}
private static char getChar(final String line, int i) {
if (line != null && line.length() > 0 && i < line.length()) {
return line.charAt(i);
}
return (char)0;
}
/**
* Try to parse a hunk header in string HEADER, putting parsed information
* into HUNK. Return TRUE if the header parsed correctly.
*/
private static boolean parseHunkHeader(String header, SVNPatchHunk hunk) {
int p = SVNPatch.ATAT.length();
if (p >= header.length() || header.charAt(p) != ' ')
/* No. */
return false;
p++;
if (p >= header.length() || header.charAt(p) != '-')
/* Nah... */
return false;
/* OK, this may be worth allocating some memory for... */
StringBuffer range = new StringBuffer(31);
p++;
while (p < header.length() && header.charAt(p) != ' ') {
range.append(header.charAt(p));
p++;
}
if (p >= header.length() || header.charAt(p) != ' ')
/* No no no... */
return false;
/* Try to parse the first range. */
if (!parseRange(hunk.original, range))
return false;
/* Clear the stringbuf so we can reuse it for the second range. */
range.setLength(0);
p++;
if (p >= header.length() || header.charAt(p) != '+')
/* Eeek! */
return false;
/* OK, this may be worth copying... */
p++;
while (p < header.length() && header.charAt(p) != ' ') {
range.append(header.charAt(p));
p++;
}
if (p >= header.length() || header.charAt(p) != ' ')
/* No no no... */
return false;
/* Check for trailing @@ */
p++;
if (p >= header.length() || !header.startsWith(SVNPatch.ATAT, p))
return false;
/*
* There may be stuff like C-function names after the trailing @@, but
* we ignore that.
*/
/* Try to parse the second range. */
if (!parseRange(hunk.modified, range))
return false;
/* Hunk header is good. */
return true;
}
/**
* Try to parse a hunk range specification from the string RANGE. Return
* parsed information in START and LENGTH, and return TRUE if the range
* parsed correctly. Note: This function may modify the input value RANGE.
*/
private static boolean parseRange(SVNPatchHunkRange hunkRange, StringBuffer range) {
int comma;
if (range.length() == 0)
return false;
comma = range.indexOf(",");
if (comma >= 0) {
if ((comma + 1) < range.length()) {
/* Try to parse the length. */
final Integer offset = parseOffset(range.substring(comma + 1));
if (offset == null) {
return false;
}
hunkRange.length = offset.intValue();
/*
* Snip off the end of the string, so we can comfortably parse
* the line number the hunk starts at.
*/
range.setLength(comma);
} else
/* A comma but no length? */
return false;
} else {
hunkRange.length = 1;
}
/* Try to parse the line number the hunk starts at. */
final Integer offset = parseOffset(range.toString());
if (offset == null) {
return false;
}
hunkRange.start = offset.intValue();
return true;
}
/**
* Try to parse a positive number from a decimal number encoded in the
* string NUMBER. Return parsed number in OFFSET, and return TRUE if parsing
* was successful.
*/
private static Integer parseOffset(String number) {
if (number != null) {
try {
return Integer.valueOf(number);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
}