From 5fd5e986823fc46a57746863ec14956e96551a36 Mon Sep 17 00:00:00 2001 From: Nadya Zabrodina Date: Mon, 1 Aug 2016 12:41:57 +0300 Subject: [PATCH] [patch]: binary patch reader implementation --- .../impl/patch/BinaryPatchContentParser.java | 83 ++++++++++ .../openapi/diff/impl/patch/PatchReader.java | 154 ++++++++++++++---- .../patch/ApplyPatchDifferentiatedDialog.java | 2 +- 3 files changed, 202 insertions(+), 37 deletions(-) create mode 100644 platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/BinaryPatchContentParser.java diff --git a/platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/BinaryPatchContentParser.java b/platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/BinaryPatchContentParser.java new file mode 100644 index 000000000000..1130abfe94d9 --- /dev/null +++ b/platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/BinaryPatchContentParser.java @@ -0,0 +1,83 @@ +/* + * Copyright 2000-2016 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.util.text.StringUtil; +import com.intellij.openapi.vcs.FileStatus; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ListIterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BinaryPatchContentParser { + @NonNls private static final Pattern ourGitBinaryLineSize = Pattern.compile("literal\\s+(\\d+)\\s*"); + + public static FilePatch readGitBinaryFormatPatch(@NotNull ListIterator iterator, @NotNull FileStatus parsedStatus) + throws PatchSyntaxException { + ByteArrayOutputStream afterStream = new ByteArrayOutputStream(); + ByteArrayOutputStream beforeStream = new ByteArrayOutputStream(); + checkNotEOF(iterator); + try { + String next = iterator.next(); + Matcher literalMatcher = ourGitBinaryLineSize.matcher(next); + if (literalMatcher.matches()) { + getContent(iterator, afterStream, literalMatcher.group(1)); + } + //parse literal before content if exist + if (iterator.hasNext() && parsedStatus != FileStatus.ADDED) { + next = iterator.next(); + if (StringUtil.isEmptyOrSpaces(next) && iterator.hasNext()) { + next = iterator.next(); + } + if (literalMatcher.reset(next).matches()) { + getContent(iterator, beforeStream, literalMatcher.group(1)); + } + else { + // does not contain 'before' literal, need to step back; + iterator.previous(); + } + } + return new BinaryFilePatch(parsedStatus == FileStatus.ADDED ? null : beforeStream.toByteArray(), + parsedStatus == FileStatus.DELETED ? null : afterStream.toByteArray()); + } + catch (Exception e) { + throw new PatchSyntaxException(iterator.previousIndex(), e.getMessage()); + } + } + + private static void getContent(@NotNull ListIterator iterator, + @NotNull ByteArrayOutputStream afterStream, @NotNull String lenFromLiteral) + throws EofBinaryPatchSyntaxException, IOException, BinaryEncoder.BinaryPatchException { + long afterSize = Long.parseLong(lenFromLiteral); + checkNotEOF(iterator); + BinaryEncoder.decode(iterator, afterSize, afterStream); + } + + private static void checkNotEOF(@NotNull ListIterator iterator) throws EofBinaryPatchSyntaxException { + if (!iterator.hasNext()) throw new EofBinaryPatchSyntaxException(iterator.previousIndex()); + } + + public static class EofBinaryPatchSyntaxException extends PatchSyntaxException { + public EofBinaryPatchSyntaxException(int line) { + super(line, "Unexpected end of binary patch"); + } + } +} + diff --git a/platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/PatchReader.java b/platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/PatchReader.java index 083497796f66..3662790ec286 100644 --- a/platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/PatchReader.java +++ b/platform/vcs-api/src/com/intellij/openapi/diff/impl/patch/PatchReader.java @@ -14,22 +14,20 @@ * 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.Couple; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.FileStatus; import com.intellij.openapi.vcs.changes.TransparentlyFailedValue; import com.intellij.openapi.vcs.changes.TransparentlyFailedValueI; import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; @@ -41,7 +39,7 @@ public class PatchReader { private final List myLines; private final PatchReader.PatchContentParser myPatchContentParser; private final AdditionalInfoParser myAdditionalInfoParser; - private List myPatches; + private List myPatches; private enum DiffFormat { CONTEXT, UNIFIED } @@ -49,12 +47,21 @@ public class PatchReader { @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); } @@ -66,20 +73,24 @@ public class PatchReader { } public List readAllPatches() throws PatchSyntaxException { - parseAllPatches(); - return myPatches; + return readTextPatches(); } - + + @NotNull + public List readTextPatches() throws PatchSyntaxException { + parseAllPatches(); + return getPatches(); + } + @Nullable public CharSequence getBaseRevision(final Project project, final String relativeFilePath) { final Map> map = myAdditionalInfoParser.getResultMap(); - if (! map.isEmpty()) { + if (!map.isEmpty()) { final Map 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()); - } + final BaseRevisionTextPatchEP baseRevisionTextPatchEP = + Extensions.findExtension(PatchEP.EP_NAME, project, BaseRevisionTextPatchEP.class); + return inner.get(baseRevisionTextPatchEP.getName()); } } return null; @@ -108,6 +119,11 @@ public class PatchReader { }*/ public List getPatches() { + return ContainerUtil.findAll(myPatches, TextFilePatch.class); + } + + @NotNull + public List getAllPatches() { return myPatches; } @@ -127,7 +143,7 @@ public class PatchReader { myAdditionalInfoParser.acceptError(new PatchSyntaxException(iterator.previousIndex(), "Contains additional information without patch itself")); } if (containsAdditionalNow) { - containsAdditional = containsAdditionalNow; + containsAdditional = true; myAdditionalInfoParser.parse(next, iterator); if (! iterator.hasNext()) { myAdditionalInfoParser.acceptError(new PatchSyntaxException(iterator.previousIndex(), "Contains additional information without patch itself")); @@ -260,10 +276,11 @@ public class PatchReader { private static class PatchContentParser implements Parser { private final boolean mySaveHunks; private DiffFormat myDiffFormat = null; - private final List myPatches; + private final List myPatches; private boolean myDiffCommandLike; private boolean myIndexLike; + private boolean myGitDiffFormat; private PatchContentParser(boolean saveHunks) { mySaveHunks = saveHunks; @@ -272,6 +289,11 @@ public class PatchReader { @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; @@ -294,23 +316,80 @@ public class PatchReader { @Override public void parse(String start, ListIterator iterator) throws PatchSyntaxException { - final TextFilePatch patch = readPatch(start, iterator); + String next = start; + if (myGitDiffFormat) { + Couple beforeAfterName = parseNamesFromGitHeaderLine(next); + FileStatus parsedStatus = FileStatus.MODIFIED; + Couple 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; } - public List getResult() throws PatchSyntaxException { + @Nullable + private static Couple 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 getResult() { return myPatches; } - private TextFilePatch readPatch(String curLine, ListIterator iterator) throws PatchSyntaxException { + private TextFilePatch readTextPatch(String curLine, ListIterator 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"); + if (!iterator.hasNext()) throw new PatchSyntaxException(iterator.previousIndex(), "Second file name expected"); curLine = iterator.next(); String secondNamePrefix = myDiffFormat == DiffFormat.UNIFIED ? "+++ " : "--- "; if (! curLine.startsWith(secondNamePrefix)) { @@ -339,13 +418,13 @@ public class PatchReader { } @Nullable - private PatchHunk readNextHunkUnified(ListIterator iterator) throws PatchSyntaxException { + private static PatchHunk readNextHunkUnified(@NotNull ListIterator iterator) throws PatchSyntaxException { String curLine = null; int numIncrements = 0; while (iterator.hasNext()) { curLine = iterator.next(); - ++ numIncrements; - if (curLine.startsWith("--- ")) { + ++numIncrements; + if (curLine.startsWith("--- ") || ourGitHeaderLinePattern.matcher(curLine).matches()) { for (int i = 0; i < numIncrements; i++) { iterator.previous(); } @@ -406,7 +485,7 @@ public class PatchReader { return null; } else { - final TextFilePatch patch = myPatches.get(myPatches.size() - 1); + final FilePatch patch = myPatches.get(myPatches.size() - 1); return patch.getBeforeName() == null ? patch.getAfterName() : patch.getBeforeName(); } } @@ -442,7 +521,7 @@ public class PatchReader { } @Nullable - private PatchHunk readNextHunkContext(ListIterator iterator) throws PatchSyntaxException { + private static PatchHunk readNextHunkContext(ListIterator iterator) throws PatchSyntaxException { while (iterator.hasNext()) { String curLine = iterator.next(); if (curLine.startsWith(CONTEXT_FILE_PREFIX)) { @@ -547,7 +626,7 @@ public class PatchReader { return patchLine; } - private List readContextDiffLines(ListIterator iterator) { + private static List readContextDiffLines(ListIterator iterator) { ArrayList result = new ArrayList<>(); while (iterator.hasNext()) { final String line = iterator.next(); @@ -579,20 +658,23 @@ public class PatchReader { } } } - if ("/dev/null".equals(fileName)) return; + String newFileName = stripPatchNameIfNeeded(fileName, gitPatch, before); + if (newFileName == null) return; if (before) { - if (gitPatch && fileName.startsWith("a/")) { - fileName = fileName.substring(2); - } - patch.setBeforeName(fileName); + patch.setBeforeName(newFileName); } else { - if (gitPatch && fileName.startsWith("b/")) { - fileName = fileName.substring(2); - } - patch.setAfterName(fileName); + 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 { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java index 2e1a357ceb40..202325f92850 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java @@ -375,7 +375,7 @@ public class ApplyPatchDifferentiatedDialog extends DialogWrapper { } final PatchReader patchReader = loadPatches(file); - List filePatches = patchReader != null ? ContainerUtil.newArrayList(patchReader.getPatches()) : Collections.emptyList(); + List filePatches = patchReader != null ? ContainerUtil.newArrayList(patchReader.getAllPatches()) : Collections.emptyList(); if (!ContainerUtil.isEmpty(myBinaryShelvedPatches)) { filePatches.addAll(myBinaryShelvedPatches); }