[patch]: binary patch reader implementation

This commit is contained in:
Nadya Zabrodina
2016-08-23 17:09:08 +03:00
committed by Nadya Zabrodina
parent 936637adcf
commit 5fd5e98682
3 changed files with 202 additions and 37 deletions
@@ -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<String> 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<String> 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<String> 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");
}
}
}
@@ -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<String> myLines;
private final PatchReader.PatchContentParser myPatchContentParser;
private final AdditionalInfoParser myAdditionalInfoParser;
private List<TextFilePatch> myPatches;
private List<FilePatch> 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<TextFilePatch> readAllPatches() throws PatchSyntaxException {
parseAllPatches();
return myPatches;
return readTextPatches();
}
@NotNull
public List<TextFilePatch> readTextPatches() throws PatchSyntaxException {
parseAllPatches();
return getPatches();
}
@Nullable
public CharSequence getBaseRevision(final Project project, final String relativeFilePath) {
final Map<String, Map<String, CharSequence>> map = myAdditionalInfoParser.getResultMap();
if (! map.isEmpty()) {
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());
}
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<TextFilePatch> getPatches() {
return ContainerUtil.findAll(myPatches, TextFilePatch.class);
}
@NotNull
public List<FilePatch> 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<TextFilePatch> myPatches;
private final List<FilePatch> 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<String> iterator) throws PatchSyntaxException {
final TextFilePatch patch = readPatch(start, iterator);
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;
}
public List<TextFilePatch> getResult() throws PatchSyntaxException {
@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 readPatch(String curLine, ListIterator<String> iterator) throws PatchSyntaxException {
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");
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<String> iterator) throws PatchSyntaxException {
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("--- ")) {
++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<String> iterator) throws PatchSyntaxException {
private static PatchHunk readNextHunkContext(ListIterator<String> 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<String> readContextDiffLines(ListIterator<String> iterator) {
private static List<String> readContextDiffLines(ListIterator<String> iterator) {
ArrayList<String> 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 {
@@ -375,7 +375,7 @@ public class ApplyPatchDifferentiatedDialog extends DialogWrapper {
}
final PatchReader patchReader = loadPatches(file);
List<FilePatch> filePatches = patchReader != null ? ContainerUtil.newArrayList(patchReader.getPatches()) : Collections.emptyList();
List<FilePatch> filePatches = patchReader != null ? ContainerUtil.newArrayList(patchReader.getAllPatches()) : Collections.emptyList();
if (!ContainerUtil.isEmpty(myBinaryShelvedPatches)) {
filePatches.addAll(myBinaryShelvedPatches);
}