| /* |
| * Copyright 2000-2009 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* |
| * Created by IntelliJ IDEA. |
| * User: yole |
| * Date: 15.11.2006 |
| * Time: 18:05:20 |
| */ |
| package com.intellij.openapi.diff.impl.patch; |
| |
| import com.intellij.openapi.extensions.Extensions; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.text.LineTokenizer; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.changes.TransparentlyFailedValue; |
| import com.intellij.openapi.vcs.changes.TransparentlyFailedValueI; |
| import com.intellij.util.SmartList; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| public class PatchReader { |
| @NonNls public static final String NO_NEWLINE_SIGNATURE = "\\ No newline at end of file"; |
| private final List<String> myLines; |
| private final PatchReader.PatchContentParser myPatchContentParser; |
| private final AdditionalInfoParser myAdditionalInfoParser; |
| private List<TextFilePatch> myPatches; |
| |
| private enum DiffFormat { CONTEXT, UNIFIED } |
| |
| @NonNls private static final String CONTEXT_HUNK_PREFIX = "***************"; |
| @NonNls private static final String CONTEXT_FILE_PREFIX = "*** "; |
| @NonNls private static final Pattern ourUnifiedHunkStartPattern = Pattern.compile("@@ -(\\d+)(,(\\d+))? \\+(\\d+)(,(\\d+))? @@.*"); |
| @NonNls private static final Pattern ourContextBeforeHunkStartPattern = Pattern.compile("\\*\\*\\* (\\d+),(\\d+) \\*\\*\\*\\*"); |
| @NonNls private static final Pattern ourContextAfterHunkStartPattern = Pattern.compile("--- (\\d+),(\\d+) ----"); |
| |
| public PatchReader(CharSequence patchContent) { |
| myLines = LineTokenizer.tokenizeIntoList(patchContent, false); |
| myAdditionalInfoParser = new AdditionalInfoParser(); |
| myPatchContentParser = new PatchContentParser(); |
| } |
| |
| public List<TextFilePatch> readAllPatches() throws PatchSyntaxException { |
| parseAllPatches(); |
| return myPatches; |
| } |
| |
| @Nullable |
| public CharSequence getBaseRevision(final Project project, final String relativeFilePath) { |
| final Map<String, Map<String, CharSequence>> map = myAdditionalInfoParser.getResultMap(); |
| if (! map.isEmpty()) { |
| final Map<String, CharSequence> inner = map.get(relativeFilePath); |
| if (inner != null) { |
| final BaseRevisionTextPatchEP baseRevisionTextPatchEP = Extensions.findExtension(PatchEP.EP_NAME, project, BaseRevisionTextPatchEP.class); |
| if (baseRevisionTextPatchEP != null) { |
| return inner.get(baseRevisionTextPatchEP.getName()); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /*private void callAdditionalInfoExtensions() { |
| final Map<String, Map<String, CharSequence>> map = myAdditionalInfoParser.getResultMap(); |
| if (! map.isEmpty()) { |
| PatchEP[] extensions = Extensions.getExtensions(PatchEP.EP_NAME, myProject); |
| final Map<String, PatchEP> byName = new HashMap<String, PatchEP>(); |
| for (PatchEP extension : extensions) { |
| byName.put(extension.getName(), extension); |
| } |
| if (extensions == null || extensions.length == 0) return; |
| for (Map.Entry<String, Map<String, CharSequence>> entry : map.entrySet()) { |
| final String path = entry.getKey(); |
| final Map<String, CharSequence> extensionToContents = entry.getValue(); |
| for (Map.Entry<String, CharSequence> innerEntry : extensionToContents.entrySet()) { |
| final PatchEP patchEP = byName.get(innerEntry.getKey()); |
| if (patchEP != null) { |
| patchEP.consumeContentBeforePatchApplied(path, innerEntry.getValue(), myCommitContext); |
| } |
| } |
| } |
| } |
| }*/ |
| |
| public List<TextFilePatch> getPatches() { |
| return myPatches; |
| } |
| |
| public void parseAllPatches() throws PatchSyntaxException { |
| final ListIterator<String> iterator = myLines.listIterator(); |
| if (! iterator.hasNext()) { |
| myPatches = Collections.emptyList(); |
| return; |
| } |
| |
| String next; |
| boolean containsAdditional = false; |
| while (iterator.hasNext()) { |
| next = iterator.next(); |
| final boolean containsAdditionalNow = myAdditionalInfoParser.testIsStart(next); |
| if (containsAdditionalNow && containsAdditional) { |
| myAdditionalInfoParser.acceptError(new PatchSyntaxException(iterator.previousIndex(), "Contains additional information without patch itself")); |
| } |
| if (containsAdditionalNow) { |
| containsAdditional = containsAdditionalNow; |
| myAdditionalInfoParser.parse(next, iterator); |
| if (! iterator.hasNext()) { |
| myAdditionalInfoParser.acceptError(new PatchSyntaxException(iterator.previousIndex(), "Contains additional information without patch itself")); |
| break; |
| } |
| next = iterator.next(); |
| } |
| |
| if (myPatchContentParser.testIsStart(next)) { |
| myPatchContentParser.parse(next, iterator); |
| //iterator.previous(); // to correctly initialize next |
| if (containsAdditional) { |
| final String lastName = myPatchContentParser.getLastName(); |
| if (lastName == null) { |
| myAdditionalInfoParser.acceptError(new PatchSyntaxException(iterator.previousIndex(), "Contains additional information without patch itself")); |
| } else { |
| myAdditionalInfoParser.copyToResult(lastName); |
| } |
| } |
| containsAdditional = false; |
| } |
| } |
| myPatches = myPatchContentParser.getResult(); |
| } |
| |
| public TransparentlyFailedValueI<Map<String, Map<String, CharSequence>>, PatchSyntaxException> getAdditionalInfo(final Set<String> filterByPaths) { |
| final TransparentlyFailedValue<Map<String, Map<String, CharSequence>>, PatchSyntaxException> |
| value = new TransparentlyFailedValue<Map<String, Map<String, CharSequence>>, PatchSyntaxException>(); |
| |
| final Map<String, Map<String, CharSequence>> map = myAdditionalInfoParser.getResultMap(); |
| final Map<String, Map<String, CharSequence>>newMap = new HashMap<String, Map<String, CharSequence>>(); |
| |
| for (Map.Entry<String, Map<String, CharSequence>> entry : map.entrySet()) { |
| final Map<String, CharSequence> innerMap = entry.getValue(); |
| if (filterByPaths == null || filterByPaths.contains(entry.getKey())) { |
| newMap.put(entry.getKey(), innerMap); |
| } |
| } |
| value.set(newMap); |
| final PatchSyntaxException e = myAdditionalInfoParser.getSyntaxException(); |
| if (e != null) { |
| value.fail(e); |
| } |
| return value; |
| } |
| |
| private static class AdditionalInfoParser implements Parser { |
| // first is path! |
| private final Map<String,Map<String, CharSequence>> myResultMap; |
| private Map<String, CharSequence> myAddMap; |
| private PatchSyntaxException mySyntaxException; |
| |
| private AdditionalInfoParser() { |
| myAddMap = new HashMap<String, CharSequence>(); |
| myResultMap = new HashMap<String, Map<String, CharSequence>>(); |
| } |
| |
| public PatchSyntaxException getSyntaxException() { |
| return mySyntaxException; |
| } |
| |
| public Map<String, Map<String, CharSequence>> getResultMap() { |
| return myResultMap; |
| } |
| |
| public void copyToResult(final String filePath) { |
| if (myAddMap != null && ! myAddMap.isEmpty()) { |
| myResultMap.put(filePath, myAddMap); |
| myAddMap = new HashMap<String, CharSequence>(); |
| } |
| } |
| |
| @Override |
| public boolean testIsStart(String start) { |
| if (mySyntaxException != null) return false; // stop on first error |
| return start != null && start.contains(UnifiedDiffWriter.ADDITIONAL_PREFIX); |
| } |
| |
| @Override |
| public void parse(String start, ListIterator<String> iterator) { |
| if (! iterator.hasNext()) { |
| mySyntaxException = new PatchSyntaxException(iterator.previousIndex(), "Empty additional info header"); |
| return; |
| } |
| while (true) { |
| final String header = iterator.next(); |
| final int idxHead = header.indexOf(UnifiedDiffWriter.ADD_INFO_HEADER); |
| if (idxHead == -1) { |
| if (myAddMap.isEmpty()) { |
| mySyntaxException = new PatchSyntaxException(iterator.previousIndex(), "Empty additional info header"); |
| } |
| iterator.previous(); |
| return; |
| } |
| |
| final String subsystem = header.substring(idxHead + UnifiedDiffWriter.ADD_INFO_HEADER.length()).trim(); |
| if (! iterator.hasNext()) { |
| mySyntaxException = new PatchSyntaxException(iterator.previousIndex(), "Empty '" + subsystem + "' data section"); |
| return; |
| } |
| |
| final StringBuilder sb = new StringBuilder(); |
| myAddMap.put(subsystem, sb); |
| while (iterator.hasNext()) { |
| final String line = iterator.next(); |
| if (! line.startsWith(UnifiedDiffWriter.ADD_INFO_LINE_START)) { |
| iterator.previous(); |
| break; |
| } |
| if (sb.length() > 0) { |
| sb.append("\n"); |
| } |
| sb.append(StringUtil.unescapeStringCharacters(line.substring(UnifiedDiffWriter.ADD_INFO_LINE_START.length()))); |
| } |
| } |
| } |
| |
| public void acceptError(PatchSyntaxException e) { |
| mySyntaxException = e; |
| } |
| } |
| |
| |
| private static class PatchContentParser implements Parser { |
| private DiffFormat myDiffFormat = null; |
| private final List<TextFilePatch> myPatches; |
| |
| private boolean myDiffCommandLike; |
| private boolean myIndexLike; |
| |
| private PatchContentParser() { |
| myPatches = new SmartList<TextFilePatch>(); |
| } |
| |
| @Override |
| public boolean testIsStart(String start) { |
| if (start.startsWith("diff")) { |
| myDiffCommandLike = true; |
| return false; |
| } |
| if (start.startsWith("index")) { |
| myIndexLike = true; |
| return false; |
| } |
| |
| if (start.startsWith("--- ") && (myDiffFormat == null || myDiffFormat == DiffFormat.UNIFIED)) { |
| myDiffFormat = DiffFormat.UNIFIED; |
| return true; |
| } |
| else if (start.startsWith(CONTEXT_FILE_PREFIX) && (myDiffFormat == null || myDiffFormat == DiffFormat.CONTEXT)) { |
| myDiffFormat = DiffFormat.CONTEXT; |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public void parse(String start, ListIterator<String> iterator) throws PatchSyntaxException { |
| final TextFilePatch patch = readPatch(start, iterator); |
| if (patch != null) { |
| myPatches.add(patch); |
| } |
| myDiffCommandLike = false; |
| myIndexLike = false; |
| } |
| |
| public List<TextFilePatch> getResult() throws PatchSyntaxException { |
| return myPatches; |
| } |
| |
| private TextFilePatch readPatch(String curLine, ListIterator<String> iterator) throws PatchSyntaxException { |
| final TextFilePatch curPatch = new TextFilePatch(null); |
| extractFileName(curLine, curPatch, true, myDiffCommandLike && myIndexLike); |
| |
| if (! iterator.hasNext()) throw new PatchSyntaxException(iterator.previousIndex(), "Second file name expected"); |
| curLine = iterator.next(); |
| String secondNamePrefix = myDiffFormat == DiffFormat.UNIFIED ? "+++ " : "--- "; |
| if (! curLine.startsWith(secondNamePrefix)) { |
| throw new PatchSyntaxException(iterator.previousIndex(), "Second file name expected"); |
| } |
| extractFileName(curLine, curPatch, false, myDiffCommandLike && myIndexLike); |
| |
| while (iterator.hasNext()) { |
| PatchHunk hunk; |
| if (myDiffFormat == DiffFormat.UNIFIED) { |
| hunk = readNextHunkUnified(iterator); |
| } |
| else { |
| hunk = readNextHunkContext(iterator); |
| } |
| if (hunk == null) break; |
| curPatch.addHunk(hunk); |
| } |
| if (curPatch.getBeforeName() == null) { |
| curPatch.setBeforeName(curPatch.getAfterName()); |
| } |
| if (curPatch.getAfterName() == null) { |
| curPatch.setAfterName(curPatch.getBeforeName()); |
| } |
| return curPatch; |
| } |
| |
| @Nullable |
| private PatchHunk readNextHunkUnified(ListIterator<String> iterator) throws PatchSyntaxException { |
| String curLine = null; |
| int numIncrements = 0; |
| while (iterator.hasNext()) { |
| curLine = iterator.next(); |
| ++ numIncrements; |
| if (curLine.startsWith("--- ")) { |
| for (int i = 0; i < numIncrements; i++) { |
| iterator.previous(); |
| } |
| return null; |
| } |
| if (curLine.startsWith("@@ ")) { |
| break; |
| } |
| } |
| if (! iterator.hasNext()) return null; |
| |
| Matcher m = ourUnifiedHunkStartPattern.matcher(curLine); |
| if (!m.matches()) { |
| throw new PatchSyntaxException(iterator.previousIndex(), "Unknown hunk start syntax"); |
| } |
| int startLineBefore = Integer.parseInt(m.group(1)); |
| final String linesBeforeText = m.group(3); |
| int linesBefore = linesBeforeText == null ? 1 : Integer.parseInt(linesBeforeText); |
| int startLineAfter = Integer.parseInt(m.group(4)); |
| final String linesAfterText = m.group(6); |
| int linesAfter = linesAfterText == null ? 1 : Integer.parseInt(linesAfterText); |
| PatchHunk hunk = new PatchHunk(startLineBefore-1, startLineBefore+linesBefore-1, startLineAfter-1, startLineAfter+linesAfter-1); |
| |
| PatchLine lastLine = null; |
| int before = 0; |
| int after = 0; |
| while (iterator.hasNext()) { |
| String hunkCurLine = iterator.next(); |
| if (lastLine != null && hunkCurLine.startsWith(NO_NEWLINE_SIGNATURE)) { |
| lastLine.setSuppressNewLine(true); |
| continue; |
| } |
| lastLine = parsePatchLine(hunkCurLine, 1, before < linesBefore || after < linesAfter); |
| if (lastLine == null) { |
| iterator.previous(); |
| break; |
| } |
| switch (lastLine.getType()) { |
| case CONTEXT: |
| before++; |
| after++; |
| break; |
| case ADD: |
| after++; |
| break; |
| case REMOVE: |
| before++; |
| break; |
| } |
| hunk.addLine(lastLine); |
| } |
| return hunk; |
| } |
| |
| @Nullable |
| public String getLastName() { |
| if (myPatches.isEmpty()) { |
| return null; |
| } |
| else { |
| final TextFilePatch patch = myPatches.get(myPatches.size() - 1); |
| return patch.getBeforeName() == null ? patch.getAfterName() : patch.getBeforeName(); |
| } |
| } |
| |
| @Nullable |
| private static PatchLine parsePatchLine(final String line, final int prefixLength) { |
| return parsePatchLine(line, prefixLength, true); |
| } |
| |
| @Nullable |
| private static PatchLine parsePatchLine(final String line, final int prefixLength, boolean expectMeaningfulLines) { |
| PatchLine.Type type; |
| if (line.startsWith("+") && expectMeaningfulLines) { |
| type = PatchLine.Type.ADD; |
| } |
| else if (line.startsWith("-") && expectMeaningfulLines) { |
| type = PatchLine.Type.REMOVE; |
| } |
| else if (line.startsWith(" ") || line.length() == 0) { |
| type = PatchLine.Type.CONTEXT; |
| } |
| else { |
| return null; |
| } |
| String lineText; |
| if (line.length() < prefixLength) { |
| lineText = ""; |
| } |
| else { |
| lineText = line.substring(prefixLength); |
| } |
| return new PatchLine(type, lineText); |
| } |
| |
| @Nullable |
| private PatchHunk readNextHunkContext(ListIterator<String> iterator) throws PatchSyntaxException { |
| while (iterator.hasNext()) { |
| String curLine = iterator.next(); |
| if (curLine.startsWith(CONTEXT_FILE_PREFIX)) { |
| iterator.previous(); |
| return null; |
| } |
| if (curLine.startsWith(CONTEXT_HUNK_PREFIX)) { |
| break; |
| } |
| } |
| if (! iterator.hasNext()) { |
| return null; |
| } |
| Matcher beforeMatcher = ourContextBeforeHunkStartPattern.matcher(iterator.next()); |
| if (! beforeMatcher.matches()) { |
| throw new PatchSyntaxException(iterator.previousIndex(), "Unknown before hunk start syntax"); |
| } |
| List<String> beforeLines = readContextDiffLines(iterator); |
| if (! iterator.hasNext()) { |
| throw new PatchSyntaxException(iterator.previousIndex(), "Missing after hunk"); |
| } |
| Matcher afterMatcher = ourContextAfterHunkStartPattern.matcher(iterator.next()); |
| if (! afterMatcher.matches()) { |
| throw new PatchSyntaxException(iterator.previousIndex(), "Unknown after hunk start syntax"); |
| } |
| //if (! iterator.hasNext()) { |
| //throw new PatchSyntaxException(iterator.previousIndex(), "Unexpected patch end"); |
| //} |
| List<String> afterLines = readContextDiffLines(iterator); |
| int startLineBefore = Integer.parseInt(beforeMatcher.group(1)); |
| int endLineBefore = Integer.parseInt(beforeMatcher.group(2)); |
| int startLineAfter = Integer.parseInt(afterMatcher.group(1)); |
| int endLineAfter = Integer.parseInt(afterMatcher.group(2)); |
| PatchHunk hunk = new PatchHunk(startLineBefore-1, endLineBefore-1, startLineAfter-1, endLineAfter-1); |
| |
| int beforeLineIndex = 0; |
| int afterLineIndex = 0; |
| PatchLine lastBeforePatchLine = null; |
| PatchLine lastAfterPatchLine = null; |
| if (beforeLines.size() == 0) { |
| for(String line: afterLines) { |
| hunk.addLine(parsePatchLine(line, 2)); |
| } |
| } |
| else if (afterLines.size() == 0) { |
| for(String line: beforeLines) { |
| hunk.addLine(parsePatchLine(line, 2)); |
| } |
| } |
| else { |
| while(beforeLineIndex < beforeLines.size() || afterLineIndex < afterLines.size()) { |
| String beforeLine = beforeLineIndex >= beforeLines.size() ? null : beforeLines.get(beforeLineIndex); |
| String afterLine = afterLineIndex >= afterLines.size() ? null : afterLines.get(afterLineIndex); |
| if (startsWith(beforeLine, NO_NEWLINE_SIGNATURE) && lastBeforePatchLine != null) { |
| lastBeforePatchLine.setSuppressNewLine(true); |
| beforeLineIndex++; |
| } |
| else if (startsWith(afterLine, NO_NEWLINE_SIGNATURE) && lastAfterPatchLine != null) { |
| lastAfterPatchLine.setSuppressNewLine(true); |
| afterLineIndex++; |
| } |
| else if (startsWith(beforeLine, " ") && |
| (startsWith(afterLine, " ") || afterLine == null /* handle some weird cases with line breaks truncated at EOF */ )) { |
| addContextDiffLine(hunk, beforeLine, PatchLine.Type.CONTEXT); |
| beforeLineIndex++; |
| afterLineIndex++; |
| } |
| else if (startsWith(beforeLine, "-")) { |
| lastBeforePatchLine = addContextDiffLine(hunk, beforeLine, PatchLine.Type.REMOVE); |
| beforeLineIndex++; |
| } |
| else if (startsWith(afterLine, "+")) { |
| lastAfterPatchLine = addContextDiffLine(hunk, afterLine, PatchLine.Type.ADD); |
| afterLineIndex++; |
| } |
| else if (startsWith(beforeLine, "!") && startsWith(afterLine, "!")) { |
| while(beforeLineIndex < beforeLines.size() && beforeLines.get(beforeLineIndex).startsWith("! ")) { |
| lastBeforePatchLine = addContextDiffLine(hunk, beforeLines.get(beforeLineIndex), PatchLine.Type.REMOVE); |
| beforeLineIndex++; |
| } |
| |
| while(afterLineIndex < afterLines.size() && afterLines.get(afterLineIndex).startsWith("! ")) { |
| lastAfterPatchLine = addContextDiffLine(hunk, afterLines.get(afterLineIndex), PatchLine.Type.ADD); |
| afterLineIndex++; |
| } |
| } |
| else { |
| throw new PatchSyntaxException(-1, "Unknown line prefix"); |
| } |
| } |
| } |
| return hunk; |
| } |
| |
| private static boolean startsWith(@Nullable final String line, final String prefix) { |
| return line != null && line.startsWith(prefix); |
| } |
| |
| private static PatchLine addContextDiffLine(final PatchHunk hunk, final String line, final PatchLine.Type type) { |
| final PatchLine patchLine = new PatchLine(type, line.length() < 2 ? "" : line.substring(2)); |
| hunk.addLine(patchLine); |
| return patchLine; |
| } |
| |
| private List<String> readContextDiffLines(ListIterator<String> iterator) { |
| ArrayList<String> result = new ArrayList<String>(); |
| while (iterator.hasNext()) { |
| final String line = iterator.next(); |
| if (!line.startsWith(" ") && !line.startsWith("+ ") && !line.startsWith("- ") && !line.startsWith("! ") && |
| !line.startsWith(NO_NEWLINE_SIGNATURE)) { |
| iterator.previous(); |
| break; |
| } |
| result.add(line); |
| } |
| return result; |
| } |
| |
| private static void extractFileName(final String curLine, final FilePatch patch, final boolean before, final boolean gitPatch) { |
| String fileName = curLine.substring(4); |
| int pos = fileName.indexOf('\t'); |
| if (pos < 0) { |
| pos = fileName.indexOf(' '); |
| } |
| if (pos >= 0) { |
| String versionId = fileName.substring(pos).trim(); |
| fileName = fileName.substring(0, pos); |
| if (versionId.length() > 0) { |
| if (before) { |
| patch.setBeforeVersionId(versionId); |
| } |
| else { |
| patch.setAfterVersionId(versionId); |
| } |
| } |
| } |
| if ("/dev/null".equals(fileName)) return; |
| if (before) { |
| if (gitPatch && fileName.startsWith("a/")) { |
| fileName = fileName.substring(2); |
| } |
| patch.setBeforeName(fileName); |
| } |
| else { |
| if (gitPatch && fileName.startsWith("b/")) { |
| fileName = fileName.substring(2); |
| } |
| patch.setAfterName(fileName); |
| } |
| } |
| } |
| |
| private interface Parser { |
| boolean testIsStart(final String start); |
| void parse(final String start, final ListIterator<String> iterator) throws PatchSyntaxException; |
| } |
| } |