/* * Copyright 2000-2015 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. */ package com.intellij.openapi.diff.impl.patch; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.FileStatus; import com.intellij.util.SmartList; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.intellij.util.containers.ContainerUtil.filter; import static com.intellij.util.containers.ContainerUtil.findAll; public class PatchReader { @NonNls public static final String NO_NEWLINE_SIGNATURE = UnifiedDiffWriter.NO_NEWLINE_SIGNATURE; private final List<String> myLines; private final PatchReader.PatchContentParser myPatchContentParser; private final AdditionalInfoParser myAdditionalInfoParser; private List<FilePatch> 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 String UNIFIED_BEFORE_HUNK_PREFIX = "--- "; @NonNls private static final String UNIFIED_AFTER_HUNK_PREFIX = "+++ "; @NonNls private static final String DIFF_GIT_HEADER_LINE = "diff --git"; @NonNls private static final String HASH_PATTERN = "[0-9a-fA-F]+"; @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+) ----"); @NonNls private static final Pattern ourEmptyRevisionInfoPattern = Pattern.compile("\\(\\s*revision\\s*\\)"); @NonNls private static final Pattern ourGitHeaderLinePattern = Pattern.compile(DIFF_GIT_HEADER_LINE + "\\s+(\\S+)\\s+(\\S+).*"); @NonNls private static final Pattern ourIndexHeaderLinePattern = Pattern.compile("index\\s+(" + HASH_PATTERN + ")..(" + HASH_PATTERN + ").*"); // need to extend with rename/copy @NonNls private static final Pattern ourFileStatusPattern = Pattern.compile("(new|deleted)\\s+file\\s+mode.*"); @NonNls private static final String ourGitBinaryContentStart = "GIT binary patch"; public PatchReader(CharSequence patchContent) { this(patchContent, true); } public PatchReader(CharSequence patchContent, boolean saveHunks) { myLines = LineTokenizer.tokenizeIntoList(patchContent, false); myAdditionalInfoParser = new AdditionalInfoParser(!saveHunks); myPatchContentParser = new PatchContentParser(saveHunks); } /** * @deprecated use {@link PatchReader#readTextPatches()} instead */ @Deprecated @NotNull public List<TextFilePatch> readAllPatches() throws PatchSyntaxException { return readTextPatches(); } @NotNull public List<TextFilePatch> readTextPatches() throws PatchSyntaxException { parseAllPatches(); return getTextPatches(); } @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); 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); } } } } }*/ /** * @deprecated use {@link PatchReader#getTextPatches()} or {@link PatchReader#getAllPatches()} instead */ @Deprecated @NotNull public List<TextFilePatch> getPatches() { return getTextPatches(); } @NotNull public List<TextFilePatch> getTextPatches() { return findAll(myPatches, TextFilePatch.class); } @NotNull public List<FilePatch> getAllPatches() { 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 = true; 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(); } @NotNull public ThrowableComputable<Map<String, Map<String, CharSequence>>, PatchSyntaxException> getAdditionalInfo(@Nullable Set<String> paths) { ThrowableComputable<Map<String, Map<String, CharSequence>>, PatchSyntaxException> result; PatchSyntaxException e = myAdditionalInfoParser.getSyntaxException(); if (e != null) { result = () -> { throw e; }; } else { Map<String, Map<String, CharSequence>> additionalInfo = filter(myAdditionalInfoParser.getResultMap(), path -> paths == null || paths.contains(path)); result = () -> additionalInfo; } return result; } private static class AdditionalInfoParser implements Parser { // first is path! private final Map<String,Map<String, CharSequence>> myResultMap; private final boolean myIgnoreMode; private Map<String, CharSequence> myAddMap; private PatchSyntaxException mySyntaxException; private AdditionalInfoParser(boolean ignore) { myIgnoreMode = ignore; myAddMap = new HashMap<>(); myResultMap = new HashMap<>(); } 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<>(); } } @Override public boolean testIsStart(String start) { if (myIgnoreMode || 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 (myIgnoreMode) { return; } 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 final boolean mySaveHunks; private DiffFormat myDiffFormat = null; private final List<FilePatch> myPatches; private boolean myDiffCommandLike; private boolean myIndexLike; private boolean myGitDiffFormat; private PatchContentParser(boolean saveHunks) { mySaveHunks = saveHunks; myPatches = new SmartList<>(); } @Override public boolean testIsStart(String start) { if (start.startsWith(DIFF_GIT_HEADER_LINE)) { myGitDiffFormat = true; myDiffCommandLike = true; return true; } 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 { String next = start; if (myGitDiffFormat) { Couple<String> beforeAfterName = parseNamesFromGitHeaderLine(next); FileStatus parsedStatus = FileStatus.MODIFIED; Couple<String> sha1Indexes = null; if (beforeAfterName == null) { throw new PatchSyntaxException(iterator.previousIndex(), "Can't detect file names from git format header line"); } while (iterator.hasNext()) { next = iterator.next(); Matcher indexMatcher = ourIndexHeaderLinePattern.matcher(next); Matcher fileStatusMatcher = ourFileStatusPattern.matcher(next); if (fileStatusMatcher.matches()) { parsedStatus = parseFileStatus(fileStatusMatcher.group(1)); } else if (indexMatcher.matches()) { myIndexLike = true; sha1Indexes = Couple.of(indexMatcher.group(1), indexMatcher.group(2)); } else { if (next.startsWith(ourGitBinaryContentStart)) { FilePatch patch = BinaryPatchContentParser.readGitBinaryFormatPatch(iterator, parsedStatus); patch.setBeforeName(beforeAfterName.getFirst()); patch.setAfterName(beforeAfterName.getSecond()); if (sha1Indexes != null) { //remember sha-1 as version ids patch.setBeforeVersionId(sha1Indexes.getFirst()); patch.setAfterVersionId(sha1Indexes.getSecond()); } addPatchAndResetSettings(patch); return; } else if (testIsStart(next)) break; } } } addPatchAndResetSettings(readTextPatch(next, iterator)); } @NotNull private static FileStatus parseFileStatus(@NotNull String status) { if (status.startsWith("new")) { return FileStatus.ADDED; } else if (status.startsWith("deleted")) return FileStatus.DELETED; return FileStatus.MODIFIED; } private void addPatchAndResetSettings(@Nullable FilePatch patch) { if (patch != null) { myPatches.add(patch); } myGitDiffFormat = false; myDiffCommandLike = false; myIndexLike = false; } @Nullable private static Couple<String> parseNamesFromGitHeaderLine(@NotNull String start) { Matcher m = ourGitHeaderLinePattern.matcher(start); return m.matches() ? Couple.of(stripPatchNameIfNeeded(m.group(1), true, true), stripPatchNameIfNeeded(m.group(2), true, false)) : null; } public List<FilePatch> getResult() { return myPatches; } private TextFilePatch readTextPatch(String curLine, ListIterator<String> iterator) throws PatchSyntaxException { final TextFilePatch curPatch = mySaveHunks ? new TextFilePatch(null) : new EmptyTextFilePatch(); 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 static PatchHunk readNextHunkUnified(@NotNull ListIterator<String> iterator) throws PatchSyntaxException { String curLine = null; int numIncrements = 0; while (iterator.hasNext()) { curLine = iterator.next(); ++numIncrements; if (curLine.startsWith("--- ") || ourGitHeaderLinePattern.matcher(curLine).matches()) { 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 FilePatch 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 static 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 static List<String> readContextDiffLines(ListIterator<String> iterator) { ArrayList<String> result = new ArrayList<>(); 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 && !ourEmptyRevisionInfoPattern.matcher(versionId).matches()) { if (before) { patch.setBeforeVersionId(versionId); } else { patch.setAfterVersionId(versionId); } } } String newFileName = stripPatchNameIfNeeded(fileName, gitPatch, before); if (newFileName == null) return; if (before) { patch.setBeforeName(newFileName); } else { patch.setAfterName(newFileName); } } @Nullable private static String stripPatchNameIfNeeded(@NotNull String fileName, boolean p1Patch, boolean before) { if ("/dev/null".equals(fileName)) return null; String prefix = before ? "a/" : "b/"; if (p1Patch && fileName.startsWith(prefix)) return fileName.substring(prefix.length()); return fileName; } } private interface Parser { boolean testIsStart(final String start); void parse(final String start, final ListIterator<String> iterator) throws PatchSyntaxException; } private static class EmptyTextFilePatch extends TextFilePatch { private int myHunkCount = 0; private boolean myNew; private boolean myDeleted; EmptyTextFilePatch() { super(null); } @Override public void addHunk(PatchHunk hunk) { if (myHunkCount == 0) { if (hunk.isNewContent()) { myNew = true; } else if (hunk.isDeletedContent()) { myDeleted = true; } } myHunkCount++; } @Override public boolean isNewFile() { return myHunkCount == 1 && myNew; } @Override public boolean isDeletedFile() { return myHunkCount == 1 && myDeleted; } } public static boolean isPatchContent(@Nullable String content) { if (content == null) return false; List<String> lines = LineTokenizer.tokenizeIntoList(content, false); final ListIterator<String> iterator = lines.listIterator(); DiffFormat currentFormat = null; while (iterator.hasNext()) { String line = iterator.next(); if (line.startsWith(CONTEXT_HUNK_PREFIX)) { currentFormat = DiffFormat.CONTEXT; } else if (currentFormat == DiffFormat.CONTEXT && ourContextBeforeHunkStartPattern.matcher(line).matches()) { break; } else if (line.startsWith(UNIFIED_BEFORE_HUNK_PREFIX) && currentFormat == null) { currentFormat = DiffFormat.UNIFIED; } else if (currentFormat == DiffFormat.UNIFIED && line.startsWith(UNIFIED_AFTER_HUNK_PREFIX)) break; } if (currentFormat == null) return false; // can't detect format // check that contains at least one chunk while (iterator.hasNext()) { String line = iterator.next(); if (currentFormat == DiffFormat.CONTEXT) { if (ourContextAfterHunkStartPattern.matcher(line).matches()) return true; } else { if (ourUnifiedHunkStartPattern.matcher(line).matches()) return true; } } return false; } }