forked from qt-creator/qt-creator
DiffEditor: Optimize patch processing
Get rid of QRegularExpressions, they are very slow. Simplify readGitPatch() a lot. Make reading of the patch about 20 times faster, especially make readGitDiff() itseft (excluding the calls to readChunks) working about 1000 times faster for huge diffs. So, the processing time for e.g. the bottom commit of qttools module (the import commit) decreased from ~20 seconds to ~1 second. Implement nice progress of patch reading. Change-Id: Ie24786596237bde475e37337663018a8bec086bb Reviewed-by: Tobias Hunger <tobias.hunger@qt.io> Reviewed-by: Orgad Shaneh <orgads@gmail.com>
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
#include "differ.h"
|
||||
|
||||
#include "texteditor/fontsettings.h"
|
||||
#include "utils/asconst.h"
|
||||
|
||||
#include <QFutureInterfaceBase>
|
||||
#include <QRegularExpression>
|
||||
@@ -695,58 +696,128 @@ static QList<RowData> readLines(QStringRef patch,
|
||||
outputRightDiffList).rows;
|
||||
}
|
||||
|
||||
static QStringRef readLine(QStringRef text, QStringRef *remainingText, bool *hasNewLine)
|
||||
{
|
||||
const QChar newLine('\n');
|
||||
const int indexOfFirstNewLine = text.indexOf(newLine);
|
||||
if (indexOfFirstNewLine < 0) {
|
||||
if (remainingText)
|
||||
*remainingText = QStringRef();
|
||||
if (hasNewLine)
|
||||
*hasNewLine = false;
|
||||
return text;
|
||||
}
|
||||
|
||||
if (hasNewLine)
|
||||
*hasNewLine = true;
|
||||
|
||||
if (remainingText)
|
||||
*remainingText = text.mid(indexOfFirstNewLine + 1);
|
||||
|
||||
return text.left(indexOfFirstNewLine);
|
||||
}
|
||||
|
||||
static bool detectChunkData(QStringRef chunkDiff,
|
||||
ChunkData *chunkData,
|
||||
QStringRef *remainingPatch)
|
||||
{
|
||||
bool hasNewLine;
|
||||
const QStringRef chunkLine = readLine(chunkDiff, remainingPatch, &hasNewLine);
|
||||
|
||||
const QLatin1String leftPosMarker("@@ -");
|
||||
const QLatin1String rightPosMarker(" +");
|
||||
const QLatin1String optionalHintMarker(" @@");
|
||||
|
||||
const int leftPosIndex = chunkLine.indexOf(leftPosMarker);
|
||||
if (leftPosIndex != 0)
|
||||
return false;
|
||||
|
||||
const int rightPosIndex = chunkLine.indexOf(rightPosMarker, leftPosIndex + leftPosMarker.size());
|
||||
if (rightPosIndex < 0)
|
||||
return false;
|
||||
|
||||
const int optionalHintIndex = chunkLine.indexOf(optionalHintMarker, rightPosIndex + rightPosMarker.size());
|
||||
if (optionalHintIndex < 0)
|
||||
return false;
|
||||
|
||||
const int leftPosStart = leftPosIndex + leftPosMarker.size();
|
||||
const int leftPosLength = rightPosIndex - leftPosStart;
|
||||
QStringRef leftPos = chunkLine.mid(leftPosStart, leftPosLength);
|
||||
|
||||
const int rightPosStart = rightPosIndex + rightPosMarker.size();
|
||||
const int rightPosLength = optionalHintIndex - rightPosStart;
|
||||
QStringRef rightPos = chunkLine.mid(rightPosStart, rightPosLength);
|
||||
|
||||
const int optionalHintStart = optionalHintIndex + optionalHintMarker.size();
|
||||
const int optionalHintLength = chunkLine.size() - optionalHintStart;
|
||||
const QStringRef optionalHint = chunkLine.mid(optionalHintStart, optionalHintLength);
|
||||
|
||||
const QChar comma(',');
|
||||
bool ok;
|
||||
|
||||
const int leftCommaIndex = leftPos.indexOf(comma);
|
||||
if (leftCommaIndex >= 0)
|
||||
leftPos = leftPos.left(leftCommaIndex);
|
||||
const int leftLineNumber = leftPos.toString().toInt(&ok);
|
||||
if (!ok)
|
||||
return false;
|
||||
|
||||
const int rightCommaIndex = rightPos.indexOf(comma);
|
||||
if (rightCommaIndex >= 0)
|
||||
rightPos = rightPos.left(rightCommaIndex);
|
||||
const int rightLineNumber = rightPos.toString().toInt(&ok);
|
||||
if (!ok)
|
||||
return false;
|
||||
|
||||
chunkData->leftStartingLineNumber = leftLineNumber - 1;
|
||||
chunkData->rightStartingLineNumber = rightLineNumber - 1;
|
||||
chunkData->contextInfo = optionalHint.toString();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static QList<ChunkData> readChunks(QStringRef patch,
|
||||
bool *lastChunkAtTheEndOfFile,
|
||||
bool *ok)
|
||||
{
|
||||
const QRegularExpression chunkRegExp(
|
||||
// beginning of the line
|
||||
"(?:\\n|^)"
|
||||
// @@ -leftPos[,leftCount] +rightPos[,rightCount] @@
|
||||
"@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@"
|
||||
// optional hint (e.g. function name)
|
||||
"(\\ +[^\\n]*)?"
|
||||
// end of line (need to be followed by text line)
|
||||
"\\n");
|
||||
|
||||
bool readOk = false;
|
||||
|
||||
QList<ChunkData> chunkDataList;
|
||||
int position = -1;
|
||||
|
||||
QRegularExpressionMatch match = chunkRegExp.match(patch);
|
||||
if (match.hasMatch() && match.capturedStart() == 0) {
|
||||
int endOfLastChunk = 0;
|
||||
do {
|
||||
const int pos = match.capturedStart();
|
||||
const int leftStartingPos = match.capturedRef(1).toInt();
|
||||
const int rightStartingPos = match.capturedRef(2).toInt();
|
||||
const QString contextInfo = match.captured(3);
|
||||
if (endOfLastChunk > 0) {
|
||||
QStringRef lines = patch.mid(endOfLastChunk,
|
||||
pos - endOfLastChunk);
|
||||
chunkDataList.last().rows = readLines(lines,
|
||||
false,
|
||||
lastChunkAtTheEndOfFile,
|
||||
&readOk);
|
||||
if (!readOk)
|
||||
break;
|
||||
}
|
||||
endOfLastChunk = match.capturedEnd();
|
||||
ChunkData chunkData;
|
||||
chunkData.leftStartingLineNumber = leftStartingPos - 1;
|
||||
chunkData.rightStartingLineNumber = rightStartingPos - 1;
|
||||
chunkData.contextInfo = contextInfo;
|
||||
chunkDataList.append(chunkData);
|
||||
match = chunkRegExp.match(patch, endOfLastChunk);
|
||||
} while (match.hasMatch());
|
||||
QVector<int> startingPositions; // store starting positions of @@
|
||||
if (patch.startsWith(QStringLiteral("@@ -")))
|
||||
startingPositions.append(position + 1);
|
||||
|
||||
if (endOfLastChunk > 0) {
|
||||
QStringRef lines = patch.mid(endOfLastChunk);
|
||||
chunkDataList.last().rows = readLines(lines,
|
||||
true,
|
||||
lastChunkAtTheEndOfFile,
|
||||
&readOk);
|
||||
}
|
||||
while ((position = patch.indexOf(QStringLiteral("\n@@ -"), position + 1)) >= 0)
|
||||
startingPositions.append(position + 1);
|
||||
|
||||
const QChar newLine('\n');
|
||||
bool readOk = true;
|
||||
|
||||
const int count = startingPositions.count();
|
||||
for (int i = 0; i < count; i++) {
|
||||
const int chunkStart = startingPositions.at(i);
|
||||
const int chunkEnd = (i < count - 1)
|
||||
// drop the newline before the next chunk start
|
||||
? startingPositions.at(i + 1) - 1
|
||||
// drop the possible newline by the end of patch
|
||||
: (patch.at(patch.count() - 1) == newLine ? patch.count() - 1 : patch.count());
|
||||
|
||||
// extract just one chunk
|
||||
const QStringRef chunkDiff = patch.mid(chunkStart, chunkEnd - chunkStart);
|
||||
|
||||
ChunkData chunkData;
|
||||
QStringRef lines;
|
||||
readOk = detectChunkData(chunkDiff, &chunkData, &lines);
|
||||
|
||||
if (!readOk)
|
||||
break;
|
||||
|
||||
chunkData.rows = readLines(lines, i == (startingPositions.size() - 1),
|
||||
lastChunkAtTheEndOfFile, &readOk);
|
||||
if (!readOk)
|
||||
break;
|
||||
|
||||
chunkDataList.append(chunkData);
|
||||
}
|
||||
|
||||
if (ok)
|
||||
@@ -881,293 +952,315 @@ static QList<FileData> readDiffPatch(QStringRef patch,
|
||||
return fileDataList;
|
||||
}
|
||||
|
||||
static bool fileNameEnd(const QChar &c)
|
||||
// The git diff patch format (ChangeFile, NewFile, DeleteFile)
|
||||
// 0. <some text lines to skip, e.g. show description>\n
|
||||
// 1. diff --git a/[fileName] b/[fileName]\n
|
||||
// 2a. new file mode [fileModeNumber]\n
|
||||
// 2b. deleted file mode [fileModeNumber]\n
|
||||
// 2c. old mode [oldFileModeNumber]\n
|
||||
// new mode [newFileModeNumber]\n
|
||||
// 2d. <Nothing, only in case of ChangeFile>
|
||||
// 3a. index [leftIndexSha]..[rightIndexSha] <optionally: octalNumber>
|
||||
// 3b. <Nothing, only in case of ChangeFile, "Dirty submodule" case>
|
||||
// 4a. <Nothing more, only possible in case of NewFile or DeleteFile> ???
|
||||
// 4b. \nBinary files [leftFileNameOrDevNull] and [rightFileNameOrDevNull] differ
|
||||
// 4c. --- [leftFileNameOrDevNull]\n
|
||||
// +++ [rightFileNameOrDevNull]\n
|
||||
// <Chunks>
|
||||
|
||||
// The git diff patch format (CopyFile, RenameFile)
|
||||
// 0. [some text lines to skip, e.g. show description]\n
|
||||
// 1. diff --git a/[leftFileName] b/[rightFileName]\n
|
||||
// 2. [dis]similarity index [0-100]%\n
|
||||
// [copy / rename] from [leftFileName]\n
|
||||
// [copy / rename] to [rightFileName]
|
||||
// 3a. <Nothing more, only when similarity index was 100%>
|
||||
// 3b. index [leftIndexSha]..[rightIndexSha] <optionally: octalNumber>
|
||||
// 4. --- [leftFileNameOrDevNull]\n
|
||||
// +++ [rightFileNameOrDevNull]\n
|
||||
// <Chunks>
|
||||
|
||||
static bool detectIndexAndBinary(QStringRef patch,
|
||||
FileData *fileData,
|
||||
QStringRef *remainingPatch)
|
||||
{
|
||||
return c == QLatin1Char('\n') || c == QLatin1Char('\t');
|
||||
bool hasNewLine;
|
||||
*remainingPatch = patch;
|
||||
|
||||
if (remainingPatch->isEmpty() && (fileData->fileOperation == FileData::CopyFile
|
||||
|| fileData->fileOperation == FileData::RenameFile)) {
|
||||
// in case of 100% similarity we don't have more lines in the patch
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringRef afterNextLine;
|
||||
// index [leftIndexSha]..[rightIndexSha] <optionally: octalNumber>
|
||||
const QStringRef nextLine = readLine(patch, &afterNextLine, &hasNewLine);
|
||||
|
||||
const QLatin1String indexHeader("index ");
|
||||
|
||||
if (nextLine.startsWith(indexHeader)) {
|
||||
const QStringRef indices = nextLine.mid(indexHeader.size());
|
||||
const int dotsPosition = indices.indexOf(QStringLiteral(".."));
|
||||
if (dotsPosition < 0)
|
||||
return false;
|
||||
fileData->leftFileInfo.typeInfo = indices.left(dotsPosition).toString();
|
||||
|
||||
// if there is no space we take the remaining string
|
||||
const int spacePosition = indices.indexOf(QChar::Space, dotsPosition + 2);
|
||||
const int length = spacePosition < 0 ? -1 : spacePosition - dotsPosition - 2;
|
||||
fileData->rightFileInfo.typeInfo = indices.mid(dotsPosition + 2, length).toString();
|
||||
|
||||
*remainingPatch = afterNextLine;
|
||||
} else if (fileData->fileOperation != FileData::ChangeFile) {
|
||||
// no index only in case of ChangeFile,
|
||||
// the dirty submodule diff case, see "Dirty Submodule" test:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remainingPatch->isEmpty() && (fileData->fileOperation == FileData::NewFile
|
||||
|| fileData->fileOperation == FileData::DeleteFile)) {
|
||||
// OK in case of empty file
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString devNull("/dev/null");
|
||||
const QString leftFileName = fileData->fileOperation == FileData::NewFile
|
||||
? devNull : QLatin1String("a/") + fileData->leftFileInfo.fileName;
|
||||
const QString rightFileName = fileData->fileOperation == FileData::DeleteFile
|
||||
? devNull : QLatin1String("b/") + fileData->rightFileInfo.fileName;
|
||||
|
||||
const QString binaryLine = QLatin1String("Binary files ")
|
||||
+ leftFileName + QLatin1String(" and ")
|
||||
+ rightFileName + QLatin1String(" differ");
|
||||
|
||||
if (*remainingPatch == binaryLine) {
|
||||
fileData->binaryFiles = true;
|
||||
*remainingPatch = QStringRef();
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString leftStart = QLatin1String("--- ") + leftFileName;
|
||||
QStringRef afterMinuses;
|
||||
// --- leftFileName
|
||||
const QStringRef minuses = readLine(*remainingPatch, &afterMinuses, &hasNewLine);
|
||||
if (!hasNewLine)
|
||||
return false; // we need to have at least one more line
|
||||
|
||||
if (!minuses.startsWith(leftStart))
|
||||
return false;
|
||||
|
||||
const QString rightStart = QLatin1String("+++ ") + rightFileName;
|
||||
QStringRef afterPluses;
|
||||
// +++ rightFileName
|
||||
const QStringRef pluses = readLine(afterMinuses, &afterPluses, &hasNewLine);
|
||||
if (!hasNewLine)
|
||||
return false; // we need to have at least one more line
|
||||
|
||||
if (!pluses.startsWith(rightStart))
|
||||
return false;
|
||||
|
||||
*remainingPatch = afterPluses;
|
||||
return true;
|
||||
}
|
||||
|
||||
static FileData readGitHeaderAndChunks(QStringRef headerAndChunks,
|
||||
const QString &fileName,
|
||||
bool *ok)
|
||||
static bool extractCommonFileName(QStringRef fileNames, QStringRef *fileName)
|
||||
{
|
||||
FileData fileData;
|
||||
fileData.leftFileInfo.fileName = fileName;
|
||||
fileData.rightFileInfo.fileName = fileName;
|
||||
// we should have 1 space between filenames
|
||||
if (fileNames.size() % 2 == 0)
|
||||
return false;
|
||||
|
||||
QStringRef patch = headerAndChunks;
|
||||
bool readOk = false;
|
||||
if (!fileNames.startsWith(QStringLiteral("a/")))
|
||||
return false;
|
||||
|
||||
const QString devNull(QLatin1String("/dev/null"));
|
||||
// drop the space in between
|
||||
const int fileNameSize = fileNames.size() / 2;
|
||||
if (!fileNames.mid(fileNameSize).startsWith(" b/"))
|
||||
return false;
|
||||
|
||||
// will be followed by: index 0000000..shasha, file "a" replaced by "/dev/null", @@ -0,0 +m,n @@
|
||||
// new file mode octal
|
||||
const QRegularExpression newFileMode("^new file mode \\d+\\n");
|
||||
// drop "a/"
|
||||
const QStringRef leftFileName = fileNames.mid(2, fileNameSize - 2);
|
||||
|
||||
// will be followed by: index shasha..0000000, file "b" replaced by "/dev/null", @@ -m,n +0,0 @@
|
||||
// deleted file mode octal
|
||||
const QRegularExpression deletedFileMode("^deleted file mode \\d+\\n");
|
||||
// drop the first filename + " b/"
|
||||
const QStringRef rightFileName = fileNames.mid(fileNameSize + 3, fileNameSize - 2);
|
||||
|
||||
const QRegularExpression modeChangeRegExp("^old mode \\d+\\nnew mode \\d+\\n");
|
||||
if (leftFileName != rightFileName)
|
||||
return false;
|
||||
|
||||
// index cap1..cap2(optionally: octal)
|
||||
const QRegularExpression indexRegExp("^index (\\w+)\\.{2}(\\w+)(?: \\d+)?(\\n|$)");
|
||||
*fileName = leftFileName;
|
||||
return true;
|
||||
}
|
||||
|
||||
QString leftFileName = QLatin1String("a/") + fileName;
|
||||
QString rightFileName = QLatin1String("b/") + fileName;
|
||||
static bool detectFileData(QStringRef patch,
|
||||
FileData *fileData,
|
||||
QStringRef *remainingPatch) {
|
||||
bool hasNewLine;
|
||||
|
||||
const QRegularExpressionMatch newFileMatch = newFileMode.match(patch);
|
||||
if (newFileMatch.hasMatch() && newFileMatch.capturedStart() == 0) {
|
||||
fileData.fileOperation = FileData::NewFile;
|
||||
leftFileName = devNull;
|
||||
patch = patch.mid(newFileMatch.capturedEnd());
|
||||
} else {
|
||||
const QRegularExpressionMatch deletedFileMatch = deletedFileMode.match(patch);
|
||||
if (deletedFileMatch.hasMatch() && deletedFileMatch.capturedStart() == 0) {
|
||||
fileData.fileOperation = FileData::DeleteFile;
|
||||
rightFileName = devNull;
|
||||
patch = patch.mid(deletedFileMatch.capturedEnd());
|
||||
QStringRef afterDiffGit;
|
||||
// diff --git a/leftFileName b/rightFileName
|
||||
const QStringRef diffGit = readLine(patch, &afterDiffGit, &hasNewLine);
|
||||
if (!hasNewLine)
|
||||
return false; // we need to have at least one more line
|
||||
|
||||
const QLatin1String gitHeader("diff --git ");
|
||||
const QStringRef fileNames = diffGit.mid(gitHeader.size());
|
||||
QStringRef commonFileName;
|
||||
if (extractCommonFileName(fileNames, &commonFileName)) {
|
||||
// change / new / delete
|
||||
|
||||
fileData->fileOperation = FileData::ChangeFile;
|
||||
fileData->leftFileInfo.fileName = fileData->rightFileInfo.fileName = commonFileName.toString();
|
||||
|
||||
QStringRef afterSecondLine;
|
||||
const QStringRef secondLine = readLine(afterDiffGit, &afterSecondLine, &hasNewLine);
|
||||
if (!hasNewLine)
|
||||
return false; // we need to have at least one more line
|
||||
|
||||
if (secondLine.startsWith(QStringLiteral("new file mode "))) {
|
||||
fileData->fileOperation = FileData::NewFile;
|
||||
*remainingPatch = afterSecondLine;
|
||||
} else if (secondLine.startsWith(QStringLiteral("deleted file mode "))) {
|
||||
fileData->fileOperation = FileData::DeleteFile;
|
||||
*remainingPatch = afterSecondLine;
|
||||
} else if (secondLine.startsWith(QStringLiteral("old mode "))) {
|
||||
QStringRef afterThirdLine;
|
||||
// new mode
|
||||
readLine(afterSecondLine, &afterThirdLine, &hasNewLine);
|
||||
if (!hasNewLine)
|
||||
return false; // we need to have at least one more line
|
||||
|
||||
// TODO: validate new mode line
|
||||
*remainingPatch = afterThirdLine;
|
||||
} else {
|
||||
const QRegularExpressionMatch modeChangeMatch = modeChangeRegExp.match(patch);
|
||||
if (modeChangeMatch.hasMatch() && modeChangeMatch.capturedStart() == 0) {
|
||||
patch = patch.mid(modeChangeMatch.capturedEnd());
|
||||
}
|
||||
*remainingPatch = afterDiffGit;
|
||||
}
|
||||
}
|
||||
|
||||
const QRegularExpressionMatch indexMatch = indexRegExp.match(patch);
|
||||
if (indexMatch.hasMatch() && indexMatch.capturedStart() == 0) {
|
||||
fileData.leftFileInfo.typeInfo = indexMatch.captured(1);
|
||||
fileData.rightFileInfo.typeInfo = indexMatch.captured(2);
|
||||
} else {
|
||||
// copy / rename
|
||||
|
||||
patch = patch.mid(indexMatch.capturedEnd());
|
||||
}
|
||||
QStringRef afterSimilarity;
|
||||
// (dis)similarity index [0-100]%
|
||||
readLine(afterDiffGit, &afterSimilarity, &hasNewLine);
|
||||
if (!hasNewLine)
|
||||
return false; // we need to have at least one more line
|
||||
|
||||
const QString binaryLine = QString::fromLatin1("Binary files ") + leftFileName
|
||||
+ QLatin1String(" and ") + rightFileName + QLatin1String(" differ");
|
||||
const QString leftStart = QString::fromLatin1("--- ") + leftFileName;
|
||||
QChar leftFollow = patch.count() > leftStart.count() ? patch.at(leftStart.count()) : QLatin1Char('\n');
|
||||
// TODO: validate similarity line
|
||||
|
||||
// empty or followed either by leftFileRegExp or by binaryRegExp
|
||||
if (patch.isEmpty() && (fileData.fileOperation == FileData::NewFile
|
||||
|| fileData.fileOperation == FileData::DeleteFile)) {
|
||||
readOk = true;
|
||||
} else if (patch.startsWith(leftStart) && fileNameEnd(leftFollow)) {
|
||||
patch = patch.mid(patch.indexOf(QLatin1Char('\n'), leftStart.count()) + 1);
|
||||
QStringRef afterCopyRenameFrom;
|
||||
// [copy / rename] from leftFileName
|
||||
const QStringRef copyRenameFrom = readLine(afterSimilarity, &afterCopyRenameFrom, &hasNewLine);
|
||||
if (!hasNewLine)
|
||||
return false; // we need to have at least one more line
|
||||
|
||||
const QString rightStart = QString::fromLatin1("+++ ") + rightFileName;
|
||||
QChar rightFollow = patch.count() > rightStart.count() ? patch.at(rightStart.count()) : QLatin1Char('\n');
|
||||
|
||||
// followed by rightFileRegExp
|
||||
if (patch.startsWith(rightStart) && fileNameEnd(rightFollow)) {
|
||||
patch = patch.mid(patch.indexOf(QLatin1Char('\n'), rightStart.count()) + 1);
|
||||
|
||||
fileData.chunks = readChunks(patch,
|
||||
&fileData.lastChunkAtTheEndOfFile,
|
||||
&readOk);
|
||||
const QLatin1String copyFrom("copy from ");
|
||||
const QLatin1String renameFrom("rename from ");
|
||||
if (copyRenameFrom.startsWith(copyFrom)) {
|
||||
fileData->fileOperation = FileData::CopyFile;
|
||||
fileData->leftFileInfo.fileName = copyRenameFrom.mid(copyFrom.size()).toString();
|
||||
} else if (copyRenameFrom.startsWith(renameFrom)) {
|
||||
fileData->fileOperation = FileData::RenameFile;
|
||||
fileData->leftFileInfo.fileName = copyRenameFrom.mid(renameFrom.size()).toString();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (patch == binaryLine) {
|
||||
readOk = true;
|
||||
fileData.binaryFiles = true;
|
||||
}
|
||||
|
||||
if (ok)
|
||||
*ok = readOk;
|
||||
QStringRef afterCopyRenameTo;
|
||||
// [copy / rename] to rightFileName
|
||||
const QStringRef copyRenameTo = readLine(afterCopyRenameFrom, &afterCopyRenameTo, &hasNewLine);
|
||||
|
||||
if (!readOk)
|
||||
return FileData();
|
||||
// if (dis)similarity index is 100% we don't have more lines
|
||||
|
||||
return fileData;
|
||||
}
|
||||
|
||||
static FileData readCopyRenameChunks(QStringRef copyRenameChunks,
|
||||
FileData::FileOperation fileOperation,
|
||||
const QString &leftFileName,
|
||||
const QString &rightFileName,
|
||||
bool *ok)
|
||||
{
|
||||
FileData fileData;
|
||||
fileData.fileOperation = fileOperation;
|
||||
fileData.leftFileInfo.fileName = leftFileName;
|
||||
fileData.rightFileInfo.fileName = rightFileName;
|
||||
|
||||
QStringRef patch = copyRenameChunks;
|
||||
bool readOk = false;
|
||||
|
||||
// index cap1..cap2(optionally: octal)
|
||||
const QRegularExpression indexRegExp("^index (\\w+)\\.{2}(\\w+)(?: \\d+)?(\\n|$)");
|
||||
|
||||
if (fileOperation == FileData::CopyFile || fileOperation == FileData::RenameFile) {
|
||||
const QRegularExpressionMatch indexMatch = indexRegExp.match(patch);
|
||||
if (indexMatch.hasMatch() && indexMatch.capturedStart() == 0) {
|
||||
fileData.leftFileInfo.typeInfo = indexMatch.captured(1);
|
||||
fileData.rightFileInfo.typeInfo = indexMatch.captured(2);
|
||||
|
||||
patch = patch.mid(indexMatch.capturedEnd());
|
||||
|
||||
const QString leftStart = QString::fromLatin1("--- a/") + leftFileName;
|
||||
QChar leftFollow = patch.count() > leftStart.count() ? patch.at(leftStart.count()) : QLatin1Char('\n');
|
||||
|
||||
// followed by leftFileRegExp
|
||||
if (patch.startsWith(leftStart) && fileNameEnd(leftFollow)) {
|
||||
patch = patch.mid(patch.indexOf(QLatin1Char('\n'), leftStart.count()) + 1);
|
||||
|
||||
// followed by rightFileRegExp
|
||||
const QString rightStart = QString::fromLatin1("+++ b/") + rightFileName;
|
||||
QChar rightFollow = patch.count() > rightStart.count() ? patch.at(rightStart.count()) : QLatin1Char('\n');
|
||||
|
||||
// followed by rightFileRegExp
|
||||
if (patch.startsWith(rightStart) && fileNameEnd(rightFollow)) {
|
||||
patch = patch.mid(patch.indexOf(QLatin1Char('\n'), rightStart.count()) + 1);
|
||||
|
||||
fileData.chunks = readChunks(patch,
|
||||
&fileData.lastChunkAtTheEndOfFile,
|
||||
&readOk);
|
||||
}
|
||||
}
|
||||
} else if (copyRenameChunks.isEmpty()) {
|
||||
readOk = true;
|
||||
const QLatin1String copyTo("copy to ");
|
||||
const QLatin1String renameTo("rename to ");
|
||||
if (fileData->fileOperation == FileData::CopyFile && copyRenameTo.startsWith(copyTo)) {
|
||||
fileData->rightFileInfo.fileName = copyRenameTo.mid(copyTo.size()).toString();
|
||||
} else if (fileData->fileOperation == FileData::RenameFile && copyRenameTo.startsWith(renameTo)) {
|
||||
fileData->rightFileInfo.fileName = copyRenameTo.mid(renameTo.size()).toString();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
*remainingPatch = afterCopyRenameTo;
|
||||
}
|
||||
|
||||
if (ok)
|
||||
*ok = readOk;
|
||||
|
||||
if (!readOk)
|
||||
return FileData();
|
||||
|
||||
return fileData;
|
||||
return detectIndexAndBinary(*remainingPatch, fileData, remainingPatch);
|
||||
}
|
||||
|
||||
static QList<FileData> readGitPatch(QStringRef patch, bool *ok,
|
||||
QFutureInterfaceBase *jobController)
|
||||
{
|
||||
int position = -1;
|
||||
|
||||
const QRegularExpression simpleGitRegExp(
|
||||
"^diff --git a/([^\\n]+) b/\\1\\n" // diff --git a/cap1 b/cap1
|
||||
, QRegularExpression::MultilineOption);
|
||||
QVector<int> startingPositions; // store starting positions of git headers
|
||||
if (patch.startsWith(QStringLiteral("diff --git ")))
|
||||
startingPositions.append(position + 1);
|
||||
|
||||
const QRegularExpression similarityRegExp(
|
||||
"^diff --git a/([^\\n]+) b/([^\\n]+)\\n" // diff --git a/cap1 b/cap2
|
||||
"(?:dis)?similarity index \\d{1,3}%\\n" // similarity / dissimilarity index xxx% (100% max)
|
||||
"(copy|rename) from \\1\\n" // copy / rename from cap1
|
||||
"\\3 to \\2\\n" // copy / rename (cap3) to cap2
|
||||
, QRegularExpression::MultilineOption);
|
||||
while ((position = patch.indexOf(QStringLiteral("\ndiff --git "), position + 1)) >= 0)
|
||||
startingPositions.append(position + 1);
|
||||
|
||||
bool readOk = false;
|
||||
|
||||
QList<FileData> fileDataList;
|
||||
|
||||
bool simpleGitMatched;
|
||||
int pos = 0;
|
||||
QRegularExpressionMatch simpleGitMatch = simpleGitRegExp.match(patch);
|
||||
QRegularExpressionMatch similarityMatch = similarityRegExp.match(patch);
|
||||
auto calculateGitMatchAndPosition = [&]() {
|
||||
if (pos > 0) { // don't advance in the initial call
|
||||
if (simpleGitMatch.hasMatch() && similarityMatch.hasMatch()) {
|
||||
const int simpleGitPos = simpleGitMatch.capturedStart();
|
||||
const int similarityPos = similarityMatch.capturedStart();
|
||||
if (simpleGitPos <= similarityPos)
|
||||
simpleGitMatch = simpleGitRegExp.match(patch, simpleGitMatch.capturedEnd() - 1); // advance only simpleGit
|
||||
else
|
||||
similarityMatch = similarityRegExp.match(patch, similarityMatch.capturedEnd() - 1); // advance only similarity
|
||||
} else if (simpleGitMatch.hasMatch()) {
|
||||
simpleGitMatch = simpleGitRegExp.match(patch, simpleGitMatch.capturedEnd() - 1);
|
||||
} else if (similarityMatch.hasMatch()) {
|
||||
similarityMatch = similarityRegExp.match(patch, similarityMatch.capturedEnd() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (simpleGitMatch.hasMatch() && similarityMatch.hasMatch()) {
|
||||
const int simpleGitPos = simpleGitMatch.capturedStart();
|
||||
const int similarityPos = similarityMatch.capturedStart();
|
||||
pos = qMin(simpleGitPos, similarityPos);
|
||||
simpleGitMatched = (pos == simpleGitPos);
|
||||
return;
|
||||
}
|
||||
|
||||
if (simpleGitMatch.hasMatch()) {
|
||||
pos = simpleGitMatch.capturedStart();
|
||||
simpleGitMatched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (similarityMatch.hasMatch()) {
|
||||
pos = similarityMatch.capturedStart();
|
||||
simpleGitMatched = false;
|
||||
return;
|
||||
}
|
||||
|
||||
pos = -1;
|
||||
simpleGitMatched = true;
|
||||
class PatchInfo {
|
||||
public:
|
||||
QStringRef patch;
|
||||
FileData fileData;
|
||||
};
|
||||
|
||||
// Set both pos and simpleGitMatched according to the first match:
|
||||
calculateGitMatchAndPosition();
|
||||
const QChar newLine('\n');
|
||||
bool readOk = true;
|
||||
|
||||
if (pos >= 0) { // git style patch
|
||||
readOk = true;
|
||||
int endOfLastHeader = 0;
|
||||
QString lastLeftFileName;
|
||||
QString lastRightFileName;
|
||||
FileData::FileOperation lastOperation = FileData::ChangeFile;
|
||||
QVector<PatchInfo> patches;
|
||||
const int count = startingPositions.count();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (jobController && jobController->isCanceled())
|
||||
return QList<FileData>();
|
||||
|
||||
auto collectFileData = [&]() {
|
||||
if (endOfLastHeader > 0 && readOk) {
|
||||
const int end = pos < 0 ? patch.count() : pos;
|
||||
QStringRef headerAndChunks = patch.mid(endOfLastHeader,
|
||||
qMax(end - endOfLastHeader - 1, 0));
|
||||
const int diffStart = startingPositions.at(i);
|
||||
const int diffEnd = (i < count - 1)
|
||||
// drop the newline before the next header start
|
||||
? startingPositions.at(i + 1) - 1
|
||||
// drop the possible newline by the end of file
|
||||
: (patch.at(patch.count() - 1) == newLine ? patch.count() - 1 : patch.count());
|
||||
|
||||
FileData fileData;
|
||||
if (lastOperation == FileData::ChangeFile) {
|
||||
fileData = readGitHeaderAndChunks(headerAndChunks,
|
||||
lastLeftFileName,
|
||||
&readOk);
|
||||
} else {
|
||||
fileData = readCopyRenameChunks(headerAndChunks,
|
||||
lastOperation,
|
||||
lastLeftFileName,
|
||||
lastRightFileName,
|
||||
&readOk);
|
||||
}
|
||||
if (readOk)
|
||||
fileDataList.append(fileData);
|
||||
}
|
||||
};
|
||||
// extract the patch for just one file
|
||||
const QStringRef fileDiff = patch.mid(diffStart, diffEnd - diffStart);
|
||||
|
||||
do {
|
||||
if (jobController && jobController->isCanceled())
|
||||
FileData fileData;
|
||||
QStringRef remainingFileDiff;
|
||||
readOk = detectFileData(fileDiff, &fileData, &remainingFileDiff);
|
||||
|
||||
if (!readOk)
|
||||
break;
|
||||
|
||||
patches.append(PatchInfo { remainingFileDiff, fileData });
|
||||
}
|
||||
|
||||
if (!readOk) {
|
||||
if (ok)
|
||||
*ok = readOk;
|
||||
return QList<FileData>();
|
||||
}
|
||||
|
||||
if (jobController)
|
||||
jobController->setProgressRange(0, patches.count());
|
||||
|
||||
QList<FileData> fileDataList;
|
||||
readOk = false;
|
||||
int i = 0;
|
||||
for (const auto &patchInfo : Utils::asConst(patches)) {
|
||||
if (jobController) {
|
||||
if (jobController->isCanceled())
|
||||
return QList<FileData>();
|
||||
jobController->setProgressValue(i++);
|
||||
}
|
||||
|
||||
collectFileData();
|
||||
if (!readOk)
|
||||
break;
|
||||
FileData fileData = patchInfo.fileData;
|
||||
if (!patchInfo.patch.isEmpty() || fileData.fileOperation == FileData::ChangeFile)
|
||||
fileData.chunks = readChunks(patchInfo.patch, &fileData.lastChunkAtTheEndOfFile, &readOk);
|
||||
else
|
||||
readOk = true;
|
||||
|
||||
if (simpleGitMatched) {
|
||||
const QString fileName = simpleGitMatch.captured(1);
|
||||
pos = simpleGitMatch.capturedEnd();
|
||||
lastLeftFileName = fileName;
|
||||
lastRightFileName = fileName;
|
||||
lastOperation = FileData::ChangeFile;
|
||||
} else {
|
||||
lastLeftFileName = similarityMatch.captured(1);
|
||||
lastRightFileName = similarityMatch.captured(2);
|
||||
const QString operation = similarityMatch.captured(3);
|
||||
pos = similarityMatch.capturedEnd();
|
||||
if (operation == QLatin1String("copy"))
|
||||
lastOperation = FileData::CopyFile;
|
||||
else if (operation == QLatin1String("rename"))
|
||||
lastOperation = FileData::RenameFile;
|
||||
else
|
||||
break; // either copy or rename, otherwise broken
|
||||
}
|
||||
endOfLastHeader = pos;
|
||||
if (!readOk)
|
||||
break;
|
||||
|
||||
// give both pos and simpleGitMatched a new value for the next match
|
||||
calculateGitMatchAndPosition();
|
||||
} while (pos != -1);
|
||||
|
||||
if (readOk)
|
||||
collectFileData();
|
||||
fileDataList.append(fileData);
|
||||
}
|
||||
|
||||
if (ok)
|
||||
|
||||
Reference in New Issue
Block a user