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:
Jarek Kobus
2017-07-13 14:52:55 +02:00
parent 178ef461bd
commit e3ce4b150d

View File

@@ -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)