summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJarek Kobus <jaroslaw.kobus@qt.io>2017-07-13 14:52:55 +0200
committerJarek Kobus <jaroslaw.kobus@qt.io>2017-08-10 16:58:45 +0000
commite3ce4b150d94f73224523da9077a8ba5fd7d04e2 (patch)
tree42818ea81e114dfb019c917ee58636459fc248b7
parent178ef461bd1ad3748307d3a43a27ca163de5afd9 (diff)
downloadqt-creator-e3ce4b150d94f73224523da9077a8ba5fd7d04e2.tar.gz
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>
-rw-r--r--src/plugins/diffeditor/diffutils.cpp657
1 files changed, 375 insertions, 282 deletions
diff --git a/src/plugins/diffeditor/diffutils.cpp b/src/plugins/diffeditor/diffutils.cpp
index 5b8ca9d674..aa4ee87027 100644
--- a/src/plugins/diffeditor/diffutils.cpp
+++ b/src/plugins/diffeditor/diffutils.cpp
@@ -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");
+ QList<ChunkData> chunkDataList;
+ int position = -1;
- bool readOk = false;
+ QVector<int> startingPositions; // store starting positions of @@
+ if (patch.startsWith(QStringLiteral("@@ -")))
+ startingPositions.append(position + 1);
- QList<ChunkData> chunkDataList;
+ while ((position = patch.indexOf(QStringLiteral("\n@@ -"), position + 1)) >= 0)
+ startingPositions.append(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());
-
- if (endOfLastChunk > 0) {
- QStringRef lines = patch.mid(endOfLastChunk);
- chunkDataList.last().rows = readLines(lines,
- true,
- lastChunkAtTheEndOfFile,
- &readOk);
- }
+ 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');
-}
-
-static FileData readGitHeaderAndChunks(QStringRef headerAndChunks,
- const QString &fileName,
- bool *ok)
-{
- FileData fileData;
- fileData.leftFileInfo.fileName = fileName;
- fileData.rightFileInfo.fileName = fileName;
-
- QStringRef patch = headerAndChunks;
- bool readOk = false;
+ bool hasNewLine;
+ *remainingPatch = patch;
- const QString devNull(QLatin1String("/dev/null"));
-
- // 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");
-
- // 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");
-
- const QRegularExpression modeChangeRegExp("^old mode \\d+\\nnew mode \\d+\\n");
-
- // index cap1..cap2(optionally: octal)
- const QRegularExpression indexRegExp("^index (\\w+)\\.{2}(\\w+)(?: \\d+)?(\\n|$)");
-
- QString leftFileName = QLatin1String("a/") + fileName;
- QString rightFileName = QLatin1String("b/") + fileName;
-
- 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());
- } else {
- const QRegularExpressionMatch modeChangeMatch = modeChangeRegExp.match(patch);
- if (modeChangeMatch.hasMatch() && modeChangeMatch.capturedStart() == 0) {
- patch = patch.mid(modeChangeMatch.capturedEnd());
- }
- }
+ 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;
}
- const QRegularExpressionMatch indexMatch = indexRegExp.match(patch);
- if (indexMatch.hasMatch() && indexMatch.capturedStart() == 0) {
- fileData.leftFileInfo.typeInfo = indexMatch.captured(1);
- fileData.rightFileInfo.typeInfo = indexMatch.captured(2);
+ 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;
+ }
- patch = patch.mid(indexMatch.capturedEnd());
+ if (remainingPatch->isEmpty() && (fileData->fileOperation == FileData::NewFile
+ || fileData->fileOperation == FileData::DeleteFile)) {
+ // OK in case of empty file
+ return true;
}
- 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');
+ 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;
- // 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);
+ const QString binaryLine = QLatin1String("Binary files ")
+ + leftFileName + QLatin1String(" and ")
+ + rightFileName + QLatin1String(" differ");
- const QString rightStart = QString::fromLatin1("+++ ") + rightFileName;
- QChar rightFollow = patch.count() > rightStart.count() ? patch.at(rightStart.count()) : QLatin1Char('\n');
+ if (*remainingPatch == binaryLine) {
+ fileData->binaryFiles = true;
+ *remainingPatch = QStringRef();
+ return true;
+ }
- // followed by rightFileRegExp
- if (patch.startsWith(rightStart) && fileNameEnd(rightFollow)) {
- patch = patch.mid(patch.indexOf(QLatin1Char('\n'), rightStart.count()) + 1);
+ 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
- fileData.chunks = readChunks(patch,
- &fileData.lastChunkAtTheEndOfFile,
- &readOk);
- }
- } else if (patch == binaryLine) {
- readOk = true;
- fileData.binaryFiles = true;
- }
+ if (!minuses.startsWith(leftStart))
+ return false;
- if (ok)
- *ok = readOk;
+ 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 (!readOk)
- return FileData();
+ if (!pluses.startsWith(rightStart))
+ return false;
- return fileData;
+ *remainingPatch = afterPluses;
+ return true;
}
-static FileData readCopyRenameChunks(QStringRef copyRenameChunks,
- FileData::FileOperation fileOperation,
- const QString &leftFileName,
- const QString &rightFileName,
- bool *ok)
+static bool extractCommonFileName(QStringRef fileNames, QStringRef *fileName)
{
- FileData fileData;
- fileData.fileOperation = fileOperation;
- fileData.leftFileInfo.fileName = leftFileName;
- fileData.rightFileInfo.fileName = rightFileName;
-
- QStringRef patch = copyRenameChunks;
- bool readOk = false;
+ // we should have 1 space between filenames
+ if (fileNames.size() % 2 == 0)
+ return false;
- // index cap1..cap2(optionally: octal)
- const QRegularExpression indexRegExp("^index (\\w+)\\.{2}(\\w+)(?: \\d+)?(\\n|$)");
+ if (!fileNames.startsWith(QStringLiteral("a/")))
+ return false;
- 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);
+ // drop the space in between
+ const int fileNameSize = fileNames.size() / 2;
+ if (!fileNames.mid(fileNameSize).startsWith(" b/"))
+ return false;
- patch = patch.mid(indexMatch.capturedEnd());
+ // drop "a/"
+ const QStringRef leftFileName = fileNames.mid(2, fileNameSize - 2);
- const QString leftStart = QString::fromLatin1("--- a/") + leftFileName;
- QChar leftFollow = patch.count() > leftStart.count() ? patch.at(leftStart.count()) : QLatin1Char('\n');
+ // drop the first filename + " b/"
+ const QStringRef rightFileName = fileNames.mid(fileNameSize + 3, fileNameSize - 2);
- // followed by leftFileRegExp
- if (patch.startsWith(leftStart) && fileNameEnd(leftFollow)) {
- patch = patch.mid(patch.indexOf(QLatin1Char('\n'), leftStart.count()) + 1);
+ if (leftFileName != rightFileName)
+ return false;
- // followed by rightFileRegExp
- const QString rightStart = QString::fromLatin1("+++ b/") + rightFileName;
- QChar rightFollow = patch.count() > rightStart.count() ? patch.at(rightStart.count()) : QLatin1Char('\n');
+ *fileName = leftFileName;
+ return true;
+}
- // followed by rightFileRegExp
- if (patch.startsWith(rightStart) && fileNameEnd(rightFollow)) {
- patch = patch.mid(patch.indexOf(QLatin1Char('\n'), rightStart.count()) + 1);
+static bool detectFileData(QStringRef patch,
+ FileData *fileData,
+ QStringRef *remainingPatch) {
+ bool hasNewLine;
+
+ 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 {
+ *remainingPatch = afterDiffGit;
+ }
- fileData.chunks = readChunks(patch,
- &fileData.lastChunkAtTheEndOfFile,
- &readOk);
- }
- }
- } else if (copyRenameChunks.isEmpty()) {
- readOk = true;
+ } else {
+ // copy / rename
+
+ QStringRef afterSimilarity;
+ // (dis)similarity index [0-100]%
+ readLine(afterDiffGit, &afterSimilarity, &hasNewLine);
+ if (!hasNewLine)
+ return false; // we need to have at least one more line
+
+ // TODO: validate similarity line
+
+ 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 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;
}
- }
- 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;
+ 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;
+ }
+ 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;
+ class PatchInfo {
+ public:
+ QStringRef patch;
+ FileData fileData;
+ };
- QList<FileData> fileDataList;
+ const QChar newLine('\n');
+ bool readOk = true;
- 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);
- }
- }
+ QVector<PatchInfo> patches;
+ const int count = startingPositions.count();
+ for (int i = 0; i < count; i++) {
+ if (jobController && jobController->isCanceled())
+ return QList<FileData>();
- if (simpleGitMatch.hasMatch() && similarityMatch.hasMatch()) {
- const int simpleGitPos = simpleGitMatch.capturedStart();
- const int similarityPos = similarityMatch.capturedStart();
- pos = qMin(simpleGitPos, similarityPos);
- simpleGitMatched = (pos == simpleGitPos);
- return;
- }
+ 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());
- if (simpleGitMatch.hasMatch()) {
- pos = simpleGitMatch.capturedStart();
- simpleGitMatched = true;
- return;
- }
+ // extract the patch for just one file
+ const QStringRef fileDiff = patch.mid(diffStart, diffEnd - diffStart);
- if (similarityMatch.hasMatch()) {
- pos = similarityMatch.capturedStart();
- simpleGitMatched = false;
- return;
- }
+ FileData fileData;
+ QStringRef remainingFileDiff;
+ readOk = detectFileData(fileDiff, &fileData, &remainingFileDiff);
- pos = -1;
- simpleGitMatched = true;
- };
+ if (!readOk)
+ break;
- // Set both pos and simpleGitMatched according to the first match:
- calculateGitMatchAndPosition();
+ patches.append(PatchInfo { remainingFileDiff, fileData });
+ }
- if (pos >= 0) { // git style patch
- readOk = true;
- int endOfLastHeader = 0;
- QString lastLeftFileName;
- QString lastRightFileName;
- FileData::FileOperation lastOperation = FileData::ChangeFile;
-
- auto collectFileData = [&]() {
- if (endOfLastHeader > 0 && readOk) {
- const int end = pos < 0 ? patch.count() : pos;
- QStringRef headerAndChunks = patch.mid(endOfLastHeader,
- qMax(end - endOfLastHeader - 1, 0));
-
- FileData fileData;
- if (lastOperation == FileData::ChangeFile) {
- fileData = readGitHeaderAndChunks(headerAndChunks,
- lastLeftFileName,
- &readOk);
- } else {
- fileData = readCopyRenameChunks(headerAndChunks,
- lastOperation,
- lastLeftFileName,
- lastRightFileName,
- &readOk);
- }
- if (readOk)
- fileDataList.append(fileData);
- }
- };
+ if (!readOk) {
+ if (ok)
+ *ok = readOk;
+ return QList<FileData>();
+ }
- do {
- if (jobController && jobController->isCanceled())
- return QList<FileData>();
+ if (jobController)
+ jobController->setProgressRange(0, patches.count());
- collectFileData();
- if (!readOk)
- break;
+ 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++);
+ }
- 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;
+ FileData fileData = patchInfo.fileData;
+ if (!patchInfo.patch.isEmpty() || fileData.fileOperation == FileData::ChangeFile)
+ fileData.chunks = readChunks(patchInfo.patch, &fileData.lastChunkAtTheEndOfFile, &readOk);
+ else
+ readOk = true;
- // give both pos and simpleGitMatched a new value for the next match
- calculateGitMatchAndPosition();
- } while (pos != -1);
+ if (!readOk)
+ break;
- if (readOk)
- collectFileData();
+ fileDataList.append(fileData);
}
if (ok)