package org.tmatesoft.svn.core.internal.wc2.patch; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNMergeRangeList; import org.tmatesoft.svn.core.SVNProperty; import org.tmatesoft.svn.core.internal.util.SVNFormatUtil; import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; import org.tmatesoft.svn.core.internal.util.SVNPathUtil; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.core.internal.wc.SVNPropertiesManager; import org.tmatesoft.svn.core.internal.wc.patch.SVNPatchFileStream; import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; import java.io.File; import java.io.IOException; import java.util.*; public class SvnPatch { private static final Transition[] TRANSITIONS = new Transition[] { new Transition("--- ", ParserState.START, IParserFunction.DIFF_MINUS), new Transition("+++ ", ParserState.MINUS_SEEN, IParserFunction.DIFF_PLUS), new Transition("diff --git", ParserState.START, IParserFunction.GIT_START), new Transition("--- a/", ParserState.GIT_DIFF_SEEN, IParserFunction.GIT_MINUS), new Transition("--- a/", ParserState.GIT_TREE_SEEN, IParserFunction.GIT_MINUS), new Transition("--- /dev/null", ParserState.GIT_TREE_SEEN, IParserFunction.GIT_MINUS), new Transition("+++ b/", ParserState.GIT_MINUS_SEEN, IParserFunction.GIT_PLUS), new Transition("+++ /dev/null", ParserState.GIT_MINUS_SEEN, IParserFunction.GIT_PLUS), new Transition("rename from ", ParserState.GIT_DIFF_SEEN, IParserFunction.GIT_MOVE_FROM), new Transition("rename to ", ParserState.MOVE_FROM_SEEN, IParserFunction.GIT_MOVE_TO), new Transition("copy from ", ParserState.GIT_DIFF_SEEN, IParserFunction.GIT_COPY_FROM), new Transition("copy to ", ParserState.COPY_FROM_SEEN, IParserFunction.GIT_COPY_TO), new Transition("new file ", ParserState.GIT_DIFF_SEEN, IParserFunction.GIT_NEW_FILE), new Transition("deleted file ", ParserState.GIT_DIFF_SEEN, IParserFunction.GIT_DELETED_FILE) }; public static SvnPatch parseNextPatch(SvnPatchFile patchFile, boolean reverse, boolean ignoreWhitespace) throws IOException, SVNException { boolean eof; long lastLine; if (patchFile.getPatchFileStream().isEOF()) { /* No more patches here. */ return null; } ParserState state = ParserState.START; boolean lineAfterTreeHeaderRead = false; SvnPatch patch = new SvnPatch(); long pos = patchFile.getNextPatchOffset(); patchFile.getPatchFileStream().setSeekPosition(pos); do { boolean validHeaderLine = false; lastLine = pos; StringBuffer lineBuffer = new StringBuffer(); eof = patchFile.getPatchFileStream().readLine(lineBuffer); String line = lineBuffer.toString(); if (!eof) { pos = patchFile.getPatchFileStream().getSeekPosition(); } for (int i = 0; i < TRANSITIONS.length; i++) { final Transition transition = TRANSITIONS[i]; if (transition.matches(line, state)) { state = transition.getParserFunction().parse(line, patch); validHeaderLine = true; break; } } if (state == ParserState.UNIDIFF_FOUND || state == ParserState.GIT_HEADER_FOUND) { break; } else if (state == ParserState.GIT_TREE_SEEN && lineAfterTreeHeaderRead) { if (!line.startsWith("index ")) { patchFile.getPatchFileStream().setSeekPosition(lastLine); break; } } else if (state == ParserState.GIT_TREE_SEEN) { lineAfterTreeHeaderRead = true; } else if (!validHeaderLine && state != ParserState.START && state != ParserState.GIT_DIFF_SEEN && !line.startsWith("index ")) { patchFile.getPatchFileStream().setSeekPosition(lastLine); state = ParserState.START; } } while (!eof); patch.setReverse(reverse); if (reverse) { File tmp = patch.getOldFileName(); patch.setOldFileName(patch.getNewFileName()); patch.setNewFileName(tmp); } if (patch.getOldFileName() == null || patch.getNewFileName() == null) { patch = null; } else { patch.parseHunks(patchFile.getPatchFileStream(), ignoreWhitespace); } patchFile.setNextPatchOffset(0); patchFile.setNextPatchOffset(patchFile.getPatchFileStream().getSeekPosition()); if (patch != null) { Collections.sort(patch.hunks); } return patch; } private List<SvnDiffHunk> hunks; private Map<String, SvnPropertiesPatch> propPatches; private SvnDiffCallback.OperationKind operation; private boolean reverse; Map<String, SVNMergeRangeList> mergeInfo; private Map reverseMergeInfo; private File oldFileName; private File newFileName; private File path; private SVNPatchFileStream patchFileStream; private void parseHunks(SVNPatchFileStream patchFileStream, boolean ignoreWhitespace) throws IOException, SVNException { String lastPropName = null; SvnDiffHunk hunk; hunks = new ArrayList<SvnDiffHunk>(); propPatches = new HashMap<String, SvnPropertiesPatch>(); do { boolean[] isProperty = new boolean[1]; String[] propName = new String[1]; SvnDiffCallback.OperationKind[] propOperation = (SvnDiffCallback.OperationKind[]) new SvnDiffCallback.OperationKind[1]; hunk = parseNextHunk(isProperty, propName, propOperation, patchFileStream, ignoreWhitespace); if (hunk != null && isProperty[0]) { if (propName[0] == null) { propName[0] = lastPropName; } else { lastPropName = propName[0]; } if (SVNProperty.MERGE_INFO.equals(propName[0])) { continue; } addPropertyHunk(propName[0], hunk, propOperation[0]); } else if (hunk != null) { hunks.add(hunk); lastPropName = null; } } while (hunk != null); } public SvnDiffHunk parseNextHunk(boolean[] isProperty, String[] propName, SvnDiffCallback.OperationKind[] propOperation, SVNPatchFileStream patchStream, boolean ignoreWhitespace) throws IOException, SVNException { final String minus = "--- "; final String textAtat = "@@"; final String propAtat = "##"; propOperation[0] = SvnDiffCallback.OperationKind.Unchanged; propName[0] = null; isProperty[0] = false; if (patchStream.isEOF()) { return null; } boolean inHunk = false; boolean hunkSeen = false; int leadingContext = 0; int trailingContext = 0; boolean changedLineSeen = false; long originalEnd = 0; long modifiedEnd = 0; long start = 0; long end = 0; int originalLines = 0; int modifiedLines = 0; SvnDiffHunk hunk = new SvnDiffHunk(); long pos = patchStream.getSeekPosition(); String line; boolean eof; long lastLine; LineType lastLineType = LineType.NOISE_LINE; do { lastLine = pos; StringBuffer lineBuffer = new StringBuffer(); eof = patchStream.readLine(lineBuffer); line = lineBuffer.toString(); pos = patchStream.getSeekPosition(); if (line.startsWith("\\")) { if (inHunk) { patchStream.setSeekPosition(lastLine - 2);//2 = max(length(EOL)) StringBuffer buffer = new StringBuffer(); String s = buffer.toString(); StringBuffer eolStrBuffer = new StringBuffer(); eof = patchStream.readLineWithEol(lineBuffer, eolStrBuffer); String eolStr = eolStrBuffer.length() == 0 ? null : eolStrBuffer.toString(); long hunkTextEnd; if (eolStr == null) { hunkTextEnd = lastLine; } else if (eolStr.charAt(0) == '\r' && eolStr.charAt(1) == '\n') { hunkTextEnd = lastLine - 2; } else if (eolStr.charAt(0) == '\n' || eolStr.charAt(0) == '\r') { hunkTextEnd = lastLine - 1; } else { hunkTextEnd = lastLine; } if (lastLineType == LineType.ORIGINAL_LINE && originalEnd == 0) { originalEnd = hunkTextEnd; } else if (lastLineType == LineType.MODIFIED_LINE && modifiedEnd == 0) { modifiedEnd = hunkTextEnd; } else if (lastLineType == LineType.CONTEXT_LINE) { if (originalEnd == 0) { originalEnd = hunkTextEnd; } if (modifiedEnd == 0) { modifiedEnd = hunkTextEnd; } } patchStream.setSeekPosition(pos); } continue; } if (inHunk && isProperty[0] && propName[0] != null && propName[0].equals(SVNProperty.MERGE_INFO)) { boolean foundMergeInfo = parseMergeInfo(line, hunk); if (foundMergeInfo) { continue; } } if (inHunk) { final char add = '+'; final char del = '-'; if (!hunkSeen) { start = lastLine; } char c = line.length() == 0 ? '\0' : line.charAt(0); if (originalLines > 0 && modifiedLines > 0 && ((c == ' ') || (!eof && line.length() == 0) || (ignoreWhitespace && c != del && c != add))) { hunkSeen = true; originalLines--; modifiedLines--; if (changedLineSeen) { trailingContext++; } else { leadingContext++; } lastLineType = LineType.CONTEXT_LINE; } else if (originalLines > 0 && c == del) { hunkSeen = true; changedLineSeen = true; if (trailingContext > 0) { trailingContext = 0; } originalLines--; lastLineType = LineType.MODIFIED_LINE; } else if (modifiedLines > 0 && c == add) { hunkSeen = true; changedLineSeen = true; if (trailingContext > 0) { trailingContext = 0; } modifiedLines--; lastLineType = LineType.MODIFIED_LINE; } else { if (eof) { end = pos; } else { end = lastLine; } if (originalEnd == 0) { originalEnd = end; } if (modifiedEnd == 0) { modifiedEnd = end; } break; } } else { if (line.startsWith(textAtat)) { inHunk = parseHunkHeader(line, hunk, textAtat); if (inHunk) { originalLines = hunk.getOriginalLength(); modifiedLines = hunk.getModifiedLength(); isProperty[0] = false; } } else if (line.startsWith(propAtat)) { inHunk = parseHunkHeader(line, hunk, propAtat); if (inHunk) { originalLines = hunk.getOriginalLength(); modifiedLines = hunk.getModifiedLength(); isProperty[0] = true; } } else if (line.startsWith("Added: ")) { propName[0] = parsePropName(line, "Added: "); if (propName[0] != null) { propOperation[0] = SvnDiffCallback.OperationKind.Added; } } else if (line.startsWith("Deleted: ")) { propName[0] = parsePropName(line, "Deleted: "); if (propName[0] != null) { propOperation[0] = SvnDiffCallback.OperationKind.Deleted; } } else if (line.startsWith("Modified: ")) { propName[0] = parsePropName(line, "Modified: "); if (propName[0] != null) { propOperation[0] = SvnDiffCallback.OperationKind.Modified; } } else if (line.startsWith(minus) || line.startsWith("diff --git ")) { break; } } } while (!eof || line.length() > 0); if (!eof) { patchStream.setSeekPosition(lastLine); } if (hunkSeen && start < end) { hunk.setPatch(this); hunk.setPatchFileStream(patchStream); hunk.setLeadingContext(leadingContext); hunk.setTrailingContext(trailingContext); hunk.setDiffTextRange(new SvnDiffHunk.Range(start, end, start)); hunk.setOriginalTextRange(new SvnDiffHunk.Range(start, originalEnd, start)); hunk.setModifiedTextRange(new SvnDiffHunk.Range(start, modifiedEnd, start)); return hunk; } else { hunk = null; } return hunk; } private boolean parseMergeInfo(String line, SvnDiffHunk hunk) throws SVNException { int slashPos = line.indexOf('/'); int colonPos = line.indexOf(':'); boolean foundMergeInfo = false; if (slashPos >= 0 && colonPos >= 0 && line.charAt(colonPos + 1) == 'r' && slashPos < colonPos) { int s = slashPos; StringBuilder input = new StringBuilder(); while (s <= colonPos) { input.append(line.charAt(s)); s++; } s++; while (s < line.length()) { if (SVNFormatUtil.isSpace(line.charAt(s))) { break; } input.append(line.charAt(s)); s++; } Map<String, SVNMergeRangeList> mergeInfoMap; try { mergeInfoMap = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(input.toString()), null); } catch (SVNException e) { if (e.getErrorMessage().getErrorCode() == SVNErrorCode.MERGE_INFO_PARSE_ERROR) { mergeInfoMap = null; } else { throw e; } } if (mergeInfoMap != null) { if (hunk.getOriginalLength() > 0) { if (isReverse()) { if (getMergeInfo() == null) { setMergeInfo(mergeInfoMap); } else { SVNMergeInfoUtil.mergeMergeInfos(getMergeInfo(), mergeInfoMap); } } else { if (getReverseMergeInfo() == null) { setReverseMergeInfo(mergeInfoMap); } else { SVNMergeInfoUtil.mergeMergeInfos(getReverseMergeInfo(), mergeInfoMap); } } hunk.decreaseOriginalLength(); } else if (hunk.getModifiedLength() > 0) { if (isReverse()) { if (getReverseMergeInfo() == null) { setReverseMergeInfo(mergeInfoMap); } else { SVNMergeInfoUtil.mergeMergeInfos(getReverseMergeInfo(), mergeInfoMap); } } else { if (getMergeInfo() == null) { setMergeInfo(mergeInfoMap); } else { SVNMergeInfoUtil.mergeMergeInfos(getMergeInfo(), mergeInfoMap); } } hunk.decreaseModifiedLength(); } foundMergeInfo = true; } } return foundMergeInfo; } private String parsePropName(String header, String indicator) throws SVNException { String propName = header.substring(indicator.length()); if (propName.length() == 0) { return null; } else if (!SVNPropertiesManager.isValidPropertyName(propName)) { propName = propName.trim(); return SVNPropertiesManager.isValidPropertyName(propName) ? propName : null; } return propName; } private boolean parseHunkHeader(String header, SvnDiffHunk hunk, String atat) { int pos = atat.length(); if (header.charAt(pos) != ' ') { return false; } pos++; if (header.charAt(pos) != '-') { return false; } StringBuilder range = new StringBuilder(); int start = ++pos; while (pos < header.length() && header.charAt(pos) != ' ') { pos++; } if (pos == header.length() || header.charAt(pos) != ' ') { return false; } range.append(header.substring(start, pos)); int[] startArray = new int[1]; int[] lengthArray = new int[1]; if (!parseRange(startArray, lengthArray, range)) { hunk.setOriginalStart(startArray[0]); hunk.setOriginalLength(lengthArray[0]); return false; } hunk.setOriginalStart(startArray[0]); hunk.setOriginalLength(lengthArray[0]); range = new StringBuilder(); pos++; if (header.charAt(pos) != '+') { return false; } start = ++pos; while (pos < header.length() && header.charAt(pos) != ' ') { pos++; } if (pos == header.length() || header.charAt(pos) != ' ') { return false; } range.append(header.substring(start, pos)); pos++; if (!header.substring(pos).startsWith(atat)) { return false; } if (!parseRange(startArray, lengthArray, range)) { hunk.setModifiedStart(startArray[0]); hunk.setModifiedLength(lengthArray[0]); return false; } hunk.setModifiedStart(startArray[0]); hunk.setModifiedLength(lengthArray[0]); return true; } private boolean parseRange(int[] start, int[] length, StringBuilder range) { if (range.length() == 0) { return false; } int commaPos = range.indexOf(","); if (commaPos >= 0) { if (range.length() > 1) { if (!parseOffset(length, range.substring(commaPos + ",".length()))) { return false; } range.setLength(commaPos); } else { return false; } } else { length[0] = 1; } return parseOffset(start, range.toString()); } private boolean parseOffset(int[] offset, String range) { final String s = range; try { offset[0] = Integer.parseInt(s); return true; } catch (NumberFormatException e) { return false; } } private void addPropertyHunk(String propName, SvnDiffHunk hunk, SvnDiffCallback.OperationKind operation) { SvnPropertiesPatch propPatch = propPatches.get(propName); if (propPatch == null) { propPatch = new SvnPropertiesPatch(propName, new ArrayList<SvnDiffHunk>(), operation); propPatches.put(propName, propPatch); } propPatch.addHunk(hunk); } public File getOldFileName() { return oldFileName; } public File getNewFileName() { return newFileName; } public List<SvnDiffHunk> getHunks() { return hunks; } public Map<String, SvnPropertiesPatch> getPropPatches() { return propPatches; } public SvnDiffCallback.OperationKind getOperation() { return operation; } public boolean isReverse() { return reverse; } public Map getMergeInfo() { return mergeInfo; } public Map getReverseMergeInfo() { return reverseMergeInfo; } public void setMergeInfo(Map<String, SVNMergeRangeList> mergeInfo) { this.mergeInfo = mergeInfo; } public void setReverseMergeInfo(Map reverseMergeInfo) { this.reverseMergeInfo = reverseMergeInfo; } public void setReverse(boolean reverse) { this.reverse = reverse; } public void setOldFileName(File oldFileName) { this.oldFileName = oldFileName; } public void setNewFileName(File newFileName) { this.newFileName = newFileName; } public void setOperation(SvnDiffCallback.OperationKind operation) { this.operation = operation; } private static File grabFileName(String s) { return SVNFileUtil.createFilePath(SVNPathUtil.canonicalizePath(s)); } private static enum ParserState { START, GIT_DIFF_SEEN, GIT_TREE_SEEN, GIT_MINUS_SEEN, GIT_PLUS_SEEN, MOVE_FROM_SEEN, COPY_FROM_SEEN, MINUS_SEEN, UNIDIFF_FOUND, GIT_HEADER_FOUND } private static interface IParserFunction { IParserFunction DIFF_MINUS = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { String s = line.split("\t")[0].substring("--- ".length()); patch.setOldFileName(grabFileName(s)); return ParserState.MINUS_SEEN; } }; IParserFunction DIFF_PLUS = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { String s = line.split("\t")[0].substring("+++ ".length()); patch.setNewFileName(grabFileName(s)); return ParserState.UNIDIFF_FOUND; } }; IParserFunction GIT_START = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { int oldPathMarkerPos = line.indexOf(" a/"); if (oldPathMarkerPos < 0) { return ParserState.START; } if (oldPathMarkerPos + 3 == line.length()) { return ParserState.START; } int newPathMarkerPos = line.indexOf(" b/"); if (newPathMarkerPos < 0) { return ParserState.START; } if (newPathMarkerPos + 3 == line.length()) { return ParserState.START; } int oldPathEndPos; int oldPathStartPos = "diff --git a/".length(); int newPathEndPos = line.length(); int newPathStartPos = oldPathStartPos; while (true) { newPathMarkerPos = line.indexOf(" b/", newPathStartPos); if (newPathMarkerPos < 0) { break; } oldPathEndPos = newPathMarkerPos; newPathStartPos = newPathMarkerPos + " b/".length(); if (newPathStartPos == line.length()) { break; } int lenOld = oldPathEndPos - oldPathStartPos; int lenNew = newPathEndPos - newPathStartPos; if (lenOld == lenNew && line.substring(oldPathStartPos, oldPathEndPos).equals(line.substring(newPathStartPos, newPathEndPos))) { patch.setOldFileName(grabFileName(line.substring(oldPathStartPos, oldPathEndPos))); patch.setNewFileName(grabFileName(line.substring(newPathStartPos))); break; } } patch.setOperation(SvnDiffCallback.OperationKind.Modified); return ParserState.GIT_DIFF_SEEN; } }; IParserFunction GIT_MINUS = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { String s = line.split("\t")[0]; if (s.startsWith("--- /dev/null")) { patch.setOldFileName(grabFileName("/dev/null")); } else { patch.setOldFileName(grabFileName(s.substring("--- a/".length()))); } return ParserState.GIT_MINUS_SEEN; } }; IParserFunction GIT_PLUS = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { String s = line.split("\t")[0]; if (s.startsWith("+++ /dev/null")) { patch.setNewFileName(grabFileName("/dev/null")); } else { patch.setNewFileName(grabFileName(s.substring("+++ b/".length()))); } return ParserState.GIT_HEADER_FOUND; } }; IParserFunction GIT_MOVE_FROM = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { patch.setOldFileName(grabFileName(line.substring("rename from ".length()))); return ParserState.MOVE_FROM_SEEN; } }; IParserFunction GIT_MOVE_TO = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { patch.setNewFileName(grabFileName(line.substring("rename to ".length()))); patch.setOperation(SvnDiffCallback.OperationKind.Moved); return ParserState.GIT_TREE_SEEN; } }; IParserFunction GIT_COPY_FROM = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { patch.setOldFileName(grabFileName(line.substring("copy from ".length()))); return ParserState.COPY_FROM_SEEN; } }; IParserFunction GIT_COPY_TO = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { patch.setNewFileName(grabFileName(line.substring("copy to ".length()))); patch.setOperation(SvnDiffCallback.OperationKind.Copied); return ParserState.GIT_TREE_SEEN; } }; IParserFunction GIT_NEW_FILE = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { patch.setOperation(SvnDiffCallback.OperationKind.Added); return ParserState.GIT_TREE_SEEN; } }; IParserFunction GIT_DELETED_FILE = new IParserFunction() { public ParserState parse(String line, SvnPatch patch) { patch.setOperation(SvnDiffCallback.OperationKind.Deleted); return ParserState.GIT_TREE_SEEN; } }; ParserState parse(String line, SvnPatch patch); } private static class Transition { private String expectedInput; private ParserState state; private IParserFunction parserFunction; public Transition(String expectedInput, ParserState state, IParserFunction parserFunction) { this.expectedInput = expectedInput; this.state = state; this.parserFunction = parserFunction; } public boolean matches(String line, ParserState state) { return line.startsWith(expectedInput) && this.state == state; } private IParserFunction getParserFunction() { return parserFunction; } } private static enum LineType { NOISE_LINE, ORIGINAL_LINE, MODIFIED_LINE, CONTEXT_LINE } }