// SPDX-FileCopyrightText: 2024 - 2025 UnionTech Software Technology Co., Ltd.//// SPDX-License-Identifier: GPL-3.0-or-later#include "diffutils.h"#include <DGuiApplicationHelper>#include <QTextBlock>#include <QTextCursor>#include <QTextDocument>#include <QRegularExpression>DGUI_USE_NAMESPACEstatic int forBlockNumber(const QMap<int, QPair<int, int>> &chunkInfo, int blockNumber,const std::function<int(int, int, int)> &func){if (chunkInfo.isEmpty())return -1;auto it = chunkInfo.upperBound(blockNumber);if (it == chunkInfo.constBegin())return -1;--it;if (blockNumber < it.key() + it.value().first)return func(it.key(), it.value().first, it.value().second);return -1;}static QList<DiffSelection> subtractSelection(const DiffSelection &minuendSelection,const DiffSelection &subtrahendSelection){// tha case that whole minuend is before the whole subtrahendif (minuendSelection.end >= 0 && minuendSelection.end <= subtrahendSelection.start)return { minuendSelection };// the case that whole subtrahend is before the whole minuendif (subtrahendSelection.end >= 0 && subtrahendSelection.end <= minuendSelection.start)return { minuendSelection };bool makeMinuendSubtrahendStart = false;bool makeSubtrahendMinuendEnd = false;if (minuendSelection.start < subtrahendSelection.start)makeMinuendSubtrahendStart = true;if (subtrahendSelection.end >= 0 && (subtrahendSelection.end < minuendSelection.end || minuendSelection.end < 0))makeSubtrahendMinuendEnd = true;QList<DiffSelection> diffList;if (makeMinuendSubtrahendStart)diffList += { minuendSelection.format, minuendSelection.start, subtrahendSelection.start };if (makeSubtrahendMinuendEnd)diffList += { minuendSelection.format, subtrahendSelection.end, minuendSelection.end };return diffList;}DiffSelections polishedSelections(const DiffSelections &selections){DiffSelections polishedSelections;for (auto it = selections.cbegin(), end = selections.cend(); it != end; ++it) {const QList<DiffSelection> diffSelections = it.value();QList<DiffSelection> workingList;for (const DiffSelection &diffSelection : diffSelections) {if (diffSelection.start == -1 && diffSelection.end == 0)continue;if (diffSelection.start == diffSelection.end && diffSelection.start >= 0)continue;int j = 0;while (j < workingList.count()) {const DiffSelection existingSelection = workingList.takeAt(j);const QList<DiffSelection> newSelection = subtractSelection(existingSelection, diffSelection);for (int k = 0; k < newSelection.count(); k++)workingList.insert(j + k, newSelection.at(k));j += newSelection.count();}workingList.append(diffSelection);}polishedSelections.insert(it.key(), workingList);}return polishedSelections;}int DiffChunkInfo::chunkIndexForBlockNumber(int blockNumber) const{return forBlockNumber(diffChunkInfo, blockNumber,[](int, int, int chunkIndex) {return chunkIndex;});}int DiffChunkInfo::chunkRowForBlockNumber(int blockNumber) const{return forBlockNumber(diffChunkInfo, blockNumber,[blockNumber](int startBlockNumber, int, int) {return blockNumber - startBlockNumber;});}int DiffChunkInfo::chunkRowsCountForBlockNumber(int blockNumber) const{return forBlockNumber(diffChunkInfo, blockNumber,[](int, int rowsCount, int) {return rowsCount;});}DiffEditorInput::DiffEditorInput(const QList<FileData> &dataList, const QFont &font): contextFileData(dataList){setFormat(font);}void DiffEditorInput::setFormat(const QFont &font){fileLineFormat.setFont(font);chunkLineFormat.setFont(font);spanLineFormat.setFont(font);lineFormat[LeftSide].setFont(font);lineFormat[RightSide].setFont(font);charFormat[LeftSide].setFont(font);charFormat[RightSide].setFont(font);if (DGuiApplicationHelper::instance()->themeType() == DGuiApplicationHelper::LightType) {fileLineFormat.setBackground(QBrush("#ffff00"));chunkLineFormat.setBackground(QBrush("#afd7e7"));lineFormat[LeftSide].setBackground(QBrush("#ffdfdf"));lineFormat[RightSide].setBackground(QBrush("#dfffdf"));charFormat[LeftSide].setBackground(QBrush("#ffafaf"));charFormat[RightSide].setBackground(QBrush("#afffaf"));} else {fileLineFormat.setForeground(QBrush("#000000"));fileLineFormat.setBackground(QBrush("#ffff00"));chunkLineFormat.setForeground(QBrush("#000000"));chunkLineFormat.setBackground(QBrush("#8aaab6"));lineFormat[LeftSide].setBackground(QBrush("#8c2d2d"));lineFormat[RightSide].setBackground(QBrush("#2d8c2d"));charFormat[LeftSide].setForeground(QBrush("#000000"));charFormat[LeftSide].setBackground(QBrush("#c34141"));charFormat[RightSide].setForeground(QBrush("#000000"));charFormat[RightSide].setBackground(QBrush("#41c341"));}}GitDiffWorker::GitDiffWorker(QObject *parent): QObject(parent){}void GitDiffWorker::stop(){isStop = true;}QList<FileData> GitDiffWorker::getPatchResult() const{return std::move(fileDataList);}SideBySideShowResults GitDiffWorker::getShowResults() const{return std::move(showResults);}void GitDiffWorker::handleParsePatch(const QString &patch){isStop = false;fileDataList.clear();readPatch(fileDataList, patch);}void GitDiffWorker::handleParseFileDatas(const QList<FileData> &dataList, const QFont &font){isStop = false;const DiffEditorInput input(dataList, font);const SideBySideDiffOutput output = diffOutput(input);if (isStop) {Q_EMIT parseFileDatasFinished(false);return;}const SideBySideShowResult leftResult { QSharedPointer<QTextDocument>(new QTextDocument()),output.side[LeftSide].diffData, output.side[LeftSide].selections };const SideBySideShowResult rightResult { QSharedPointer<QTextDocument>(new QTextDocument()),output.side[RightSide].diffData, output.side[RightSide].selections };const SideBySideShowResults result { leftResult, rightResult };auto propagateDocument = [&output, this](DiffSide side, const SideBySideShowResult &result) {// No need to store the change historyresult.textDocument->setUndoRedoEnabled(false);// We could do just:// result.textDocument->setPlainText(output.diffText);// but this would freeze the thread for couple of seconds without progress reporting// and without checking for canceled.const int diffSize = output.side[side].diffText.size();const int packageSize = 10000;int currentPos = 0;QTextCursor cursor(result.textDocument.data());while (currentPos < diffSize) {const QString package = output.side[side].diffText.mid(currentPos, packageSize);cursor.insertText(package);currentPos += package.size();if (isStop) {Q_EMIT parseFileDatasFinished(false);return;}}// If future was canceled, the destructor runs in this thread, so we can't move it// to caller's thread. We push it to no thread (make object to have no thread affinity),// and later, in the caller's thread, we pull it back to the caller's thread.result.textDocument->moveToThread(nullptr);};propagateDocument(LeftSide, leftResult);if (isStop) {Q_EMIT parseFileDatasFinished(false);return;}propagateDocument(RightSide, rightResult);if (isStop) {Q_EMIT parseFileDatasFinished(false);return;}showResults = result;Q_EMIT parseFileDatasFinished(true);}QList<ChunkData> GitDiffWorker::readChunks(const QString &patch, bool *lastChunkAtTheEndOfFile, bool *ok){QList<ChunkData> chunkDataList;int position = -1;QList<int> startingPositions; // store starting positions of @@if (patch.startsWith(QStringLiteral("@@ -")))startingPositions.append(position + 1);while ((position = patch.indexOf(QStringLiteral("\n@@ -"), position + 1)) >= 0)startingPositions.append(position + 1);const QChar newLine('\n');bool readOk = true;const int count = startingPositions.size();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.size() - 1) == newLine ? patch.size() - 1: patch.size());// extract just one chunkconst QString chunkDiff = patch.mid(chunkStart, chunkEnd - chunkStart);ChunkData chunkData;QString 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)*ok = readOk;return chunkDataList;}void GitDiffWorker::readGitPatch(QList<FileData> &dataList, const QString &patch){int position = -1;QList<int> startingPositions; // store starting positions of git headersif (patch.startsWith(QStringLiteral("diff --git ")))startingPositions.append(position + 1);while ((position = patch.indexOf(QStringLiteral("\ndiff --git "), position + 1)) >= 0)startingPositions.append(position + 1);class PatchInfo{public:QString patch;FileData fileData;};const QChar newLine('\n');QList<PatchInfo> patches;const int count = startingPositions.size();for (int i = 0; i < count; i++) {if (isStop)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.size() - 1) == newLine ? patch.size() - 1: patch.size());// extract the patch for just one fileconst QString fileDiff = patch.mid(diffStart, diffEnd - diffStart);FileData fileData;QString remainingFileDiff;if (!detectFileData(fileDiff, &fileData, &remainingFileDiff))return;patches.append(PatchInfo { remainingFileDiff, fileData });}if (patches.isEmpty())return;for (const auto &patchInfo : std::as_const(patches)) {if (isStop)return;FileData fileData = patchInfo.fileData;bool readOk = false;if (!patchInfo.patch.isEmpty() || fileData.fileOperation == FileData::ChangeFile)fileData.chunks = readChunks(patchInfo.patch, &fileData.lastChunkAtTheEndOfFile, &readOk);elsereadOk = true;if (!readOk)return;dataList.append(fileData);}}void GitDiffWorker::readPatch(QList<FileData> &dataList, const QString &patch){QString tmpPath = patch;const QRegularExpression formatPatchEndingRegExp("(\\n-- \\n\\S*\\n\\n$)");auto match = formatPatchEndingRegExp.match(tmpPath);if (match.hasMatch())tmpPath = tmpPath.left(match.capturedStart() + 1);readGitPatch(dataList, tmpPath);if (dataList.isEmpty())readDiffPatch(dataList, tmpPath);Q_EMIT parsePatchFinished();}void GitDiffWorker::readDiffPatch(QList<FileData> &dataList, const QString &patch){const QRegularExpression diffRegExp("(?:\\n|^)" // new line of the beginning of a patch"(" // either"-{3} " // ---"[^\\t\\n]+" // filename1"(?:\\t[^\\n]*)*\\n" // optionally followed by: \t anything \t anything ..."\\+{3} " // +++"[^\\t\\n]+" // filename2"(?:\\t[^\\n]*)*\\n" // optionally followed by: \t anything \t anything ..."|" // or"Binary files ""[^\\t\\n]+" // filename1" and ""[^\\t\\n]+" // filename2" differ"")"); // end of orbool readOk = false;QRegularExpressionMatch diffMatch = diffRegExp.match(patch);if (diffMatch.hasMatch()) {readOk = true;int lastPos = -1;do {if (isStop)return;int pos = diffMatch.capturedStart();if (lastPos >= 0) {QString headerAndChunks = patch.mid(lastPos, pos - lastPos);const FileData fileData = readDiffHeaderAndChunks(headerAndChunks, &readOk);if (!readOk)break;dataList.append(fileData);}lastPos = pos;pos = diffMatch.capturedEnd();diffMatch = diffRegExp.match(patch, pos);} while (diffMatch.hasMatch());if (readOk) {QString headerAndChunks = patch.mid(lastPos, patch.size() - lastPos - 1);const FileData fileData = readDiffHeaderAndChunks(headerAndChunks, &readOk);if (readOk)dataList.append(fileData);}}if (!readOk)return;}FileData GitDiffWorker::readDiffHeaderAndChunks(const QString &headerAndChunks, bool *ok){QString patch = headerAndChunks;FileData fileData;bool readOk = false;const QRegularExpression leftFileRegExp("(?:\\n|^)-{3} " // "--- ""([^\\t\\n]+)" // "fileName1""(?:\\t[^\\n]*)*\\n"); // optionally followed by: \t anything \t anything ...)const QRegularExpression rightFileRegExp("^\\+{3} " // "+++ ""([^\\t\\n]+)" // "fileName2""(?:\\t[^\\n]*)*\\n"); // optionally followed by: \t anything \t anything ...)const QRegularExpression binaryRegExp("^Binary files ([^\\t\\n]+) and ([^\\t\\n]+) differ$");// followed either by leftFileRegExpconst QRegularExpressionMatch leftMatch = leftFileRegExp.match(patch);if (leftMatch.hasMatch() && leftMatch.capturedStart() == 0) {patch = patch.mid(leftMatch.capturedEnd());fileData.fileInfo[LeftSide].fileName = leftMatch.captured(1);// followed by rightFileRegExpconst QRegularExpressionMatch rightMatch = rightFileRegExp.match(patch);if (rightMatch.hasMatch() && rightMatch.capturedStart() == 0) {patch = patch.mid(rightMatch.capturedEnd());fileData.fileInfo[RightSide].fileName = rightMatch.captured(1);fileData.chunks = readChunks(patch,&fileData.lastChunkAtTheEndOfFile,&readOk);}} else {// or by binaryRegExpconst QRegularExpressionMatch binaryMatch = binaryRegExp.match(patch);if (binaryMatch.hasMatch() && binaryMatch.capturedStart() == 0) {fileData.fileInfo[LeftSide].fileName = binaryMatch.captured(1);fileData.fileInfo[RightSide].fileName = binaryMatch.captured(2);fileData.binaryFiles = true;readOk = true;}}if (ok)*ok = readOk;if (!readOk)return {};return fileData;}ChunkData GitDiffWorker::calculateOriginalData(const QList<Diff> &leftDiffList,const QList<Diff> &rightDiffList){int i = 0;int j = 0;QList<TextLineData> leftLines;QList<TextLineData> rightLines;// <line number, span count>QMap<int, int> leftSpans;QMap<int, int> rightSpans;// <left line number, right line number>QMap<int, int> equalLines;int leftLineNumber = 0;int rightLineNumber = 0;int leftLineAligned = -1;int rightLineAligned = -1;bool lastLineEqual = true;while (i <= leftDiffList.size() && j <= rightDiffList.size()) {const Diff leftDiff = i < leftDiffList.size() ? leftDiffList.at(i) : Diff(Diff::Equal);const Diff rightDiff = j < rightDiffList.size() ? rightDiffList.at(j) : Diff(Diff::Equal);if (leftDiff.command == Diff::Delete) {if (j == rightDiffList.size() && lastLineEqual && leftDiff.text.startsWith('\n'))equalLines.insert(leftLineNumber, rightLineNumber);// process deletehandleDifference(leftDiff.text, &leftLines, &leftLineNumber);lastLineEqual = lastLinesEqual(leftLines, rightLines);if (j == rightDiffList.size())lastLineEqual = false;i++;}if (rightDiff.command == Diff::Insert) {if (i == leftDiffList.size() && lastLineEqual && rightDiff.text.startsWith('\n'))equalLines.insert(leftLineNumber, rightLineNumber);// process inserthandleDifference(rightDiff.text, &rightLines, &rightLineNumber);lastLineEqual = lastLinesEqual(leftLines, rightLines);if (i == leftDiffList.size())lastLineEqual = false;j++;}if (leftDiff.command == Diff::Equal && rightDiff.command == Diff::Equal) {// process equalconst QStringList newLeftLines = leftDiff.text.split('\n');const QStringList newRightLines = rightDiff.text.split('\n');int line = 0;if (i < leftDiffList.size() || j < rightDiffList.size()|| (!leftLines.isEmpty() && !rightLines.isEmpty())) {while (line < qMax(newLeftLines.size(), newRightLines.size())) {handleLine(newLeftLines, line, &leftLines, &leftLineNumber);handleLine(newRightLines, line, &rightLines, &rightLineNumber);const int commonLineCount = qMin(newLeftLines.size(), newRightLines.size());if (line < commonLineCount) {// try to alignconst int leftDifference = leftLineNumber - leftLineAligned;const int rightDifference = rightLineNumber - rightLineAligned;if (leftDifference && rightDifference) {bool doAlign = true;if (line == 0&& (newLeftLines.at(0).isEmpty()|| newRightLines.at(0).isEmpty())&& !lastLineEqual) {// omit alignment when first lines of equalities// are empty and last generated lines are not equaldoAlign = false;}if (line == commonLineCount - 1) {// omit alignment when last lines of equalities are emptyif (leftLines.last().text.isEmpty()|| rightLines.last().text.isEmpty())doAlign = false;// unless it's the last dummy line (don't omit in that case)if (i == leftDiffList.size() && j == rightDiffList.size())doAlign = true;}if (doAlign) {// align hereleftLineAligned = leftLineNumber;rightLineAligned = rightLineNumber;// insert separators if neededif (rightDifference > leftDifference)leftSpans.insert(leftLineNumber,rightDifference - leftDifference);else if (leftDifference > rightDifference)rightSpans.insert(rightLineNumber,leftDifference - rightDifference);}}}// check if lines are equalif ((line < commonLineCount - 1) // before the last common line in equality|| (line == commonLineCount - 1 // or the last common line in equality&& i == leftDiffList.size() // and it's the last iteration&& j == rightDiffList.size())) {if (line > 0 || lastLineEqual)equalLines.insert(leftLineNumber, rightLineNumber);}if (line > 0)lastLineEqual = true;line++;}}i++;j++;}}QList<TextLineData> leftData = assemblyRows(leftLines, leftSpans);QList<TextLineData> rightData = assemblyRows(rightLines, rightSpans);// fill ending separatorsfor (int i = leftData.size(); i < rightData.size(); i++)leftData.append(TextLineData(TextLineData::Separator));for (int i = rightData.size(); i < leftData.size(); i++)rightData.append(TextLineData(TextLineData::Separator));const int visualLineCount = leftData.size();int leftLine = -1;int rightLine = -1;ChunkData chunkData;for (int i = 0; i < visualLineCount; i++) {const TextLineData &leftTextLine = leftData.at(i);const TextLineData &rightTextLine = rightData.at(i);RowData row(leftTextLine, rightTextLine);if (leftTextLine.textLineType == TextLineData::TextLine)++leftLine;if (rightTextLine.textLineType == TextLineData::TextLine)++rightLine;if (equalLines.value(leftLine, -2) == rightLine)row.equal = true;chunkData.rows.append(row);}return chunkData;}bool GitDiffWorker::detectChunkData(const QString &chunkDiff, ChunkData *chunkData, QString *remainingPatch){bool hasNewLine;const QString 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;QString leftPos = chunkLine.mid(leftPosStart, leftPosLength);const int rightPosStart = rightPosIndex + rightPosMarker.size();const int rightPosLength = optionalHintIndex - rightPosStart;QString rightPos = chunkLine.mid(rightPosStart, rightPosLength);const int optionalHintStart = optionalHintIndex + optionalHintMarker.size();const int optionalHintLength = chunkLine.size() - optionalHintStart;const QString 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.toInt(&ok);if (!ok)return false;const int rightCommaIndex = rightPos.indexOf(comma);if (rightCommaIndex >= 0)rightPos = rightPos.left(rightCommaIndex);const int rightLineNumber = rightPos.toInt(&ok);if (!ok)return false;chunkData->startingLineNumber = { leftLineNumber - 1, rightLineNumber - 1 };chunkData->contextInfo = optionalHint;return true;}QString GitDiffWorker::readLine(const QString &text, QString *remainingText, bool *hasNewLine){const QChar newLine('\n');const int indexOfFirstNewLine = text.indexOf(newLine);if (indexOfFirstNewLine < 0) {if (remainingText)*remainingText = QString();if (hasNewLine)*hasNewLine = false;return text;}if (hasNewLine)*hasNewLine = true;if (remainingText)*remainingText = text.mid(indexOfFirstNewLine + 1);return text.left(indexOfFirstNewLine);}QList<RowData> GitDiffWorker::readLines(const QString &patch, bool lastChunk, bool *lastChunkAtTheEndOfFile, bool *ok){QList<Diff> diffList;const QChar newLine = '\n';int lastEqual = -1;int lastDelete = -1;int lastInsert = -1;int noNewLineInEqual = -1;int noNewLineInDelete = -1;int noNewLineInInsert = -1;const QList<QString> lines = patch.split(newLine);int i;for (i = 0; i < lines.size(); i++) {QString line = lines.at(i);if (line.isEmpty()) { // need to have at least one character (1 column)if (lastChunk)i = lines.size(); // pretend as we've read all the lines (we just ignore the rest)break;}const QChar firstCharacter = line.at(0);if (firstCharacter == '\\') { // no new line markerif (!lastChunk) // can only appear in last chunk of the filebreak;if (!diffList.isEmpty()) {Diff &last = diffList.last();if (last.text.isEmpty())break;if (last.command == Diff::Equal) {if (noNewLineInEqual >= 0)break;noNewLineInEqual = diffList.size() - 1;} else if (last.command == Diff::Delete) {if (noNewLineInDelete >= 0)break;noNewLineInDelete = diffList.size() - 1;} else if (last.command == Diff::Insert) {if (noNewLineInInsert >= 0)break;noNewLineInInsert = diffList.size() - 1;}}} else {Diff::Command command = Diff::Equal;if (firstCharacter == ' ') { // common linecommand = Diff::Equal;} else if (firstCharacter == '-') { // deleted linecommand = Diff::Delete;} else if (firstCharacter == '+') { // inserted linecommand = Diff::Insert;} else { // no other character may exist as the first characterif (lastChunk)i = lines.size(); // pretend as we've read all the lines (we just ignore the rest)break;}Diff diffToBeAdded(command, line.mid(1) + newLine);if (!diffList.isEmpty() && diffList.last().command == command)diffList.last().text.append(diffToBeAdded.text);elsediffList.append(diffToBeAdded);if (command == Diff::Equal) // common linelastEqual = diffList.size() - 1;else if (command == Diff::Delete) // deleted linelastDelete = diffList.size() - 1;else if (command == Diff::Insert) // inserted linelastInsert = diffList.size() - 1;}}if (i < lines.size() // we broke before// or we have noNewLine in some equal line and in either delete or insert line|| (noNewLineInEqual >= 0 && (noNewLineInDelete >= 0 || noNewLineInInsert >= 0))// or we have noNewLine in not the last equal line|| (noNewLineInEqual >= 0 && noNewLineInEqual != lastEqual)// or we have noNewLine in not the last delete line or there is a equal line after the noNewLine for delete|| (noNewLineInDelete >= 0 && (noNewLineInDelete != lastDelete || lastEqual > lastDelete))// or we have noNewLine in not the last insert line or there is a equal line after the noNewLine for insert|| (noNewLineInInsert >= 0 && (noNewLineInInsert != lastInsert || lastEqual > lastInsert))) {if (ok)*ok = false;return {};}if (ok)*ok = true;bool removeNewLineFromLastEqual = false;bool removeNewLineFromLastDelete = false;bool removeNewLineFromLastInsert = false;bool prependNewLineAfterLastEqual = false;if (noNewLineInDelete >= 0 || noNewLineInInsert >= 0) {if (noNewLineInDelete >= 0)removeNewLineFromLastDelete = true;if (noNewLineInInsert >= 0)removeNewLineFromLastInsert = true;} else {if (noNewLineInEqual >= 0) {removeNewLineFromLastEqual = true;} else {if (lastEqual > lastDelete && lastEqual > lastInsert) {removeNewLineFromLastEqual = true;} else if (lastDelete > lastEqual && lastDelete > lastInsert) {if (lastInsert > lastEqual) {removeNewLineFromLastDelete = true;removeNewLineFromLastInsert = true;} else if (lastEqual > lastInsert) {removeNewLineFromLastEqual = true;removeNewLineFromLastDelete = true;prependNewLineAfterLastEqual = true;}} else if (lastInsert > lastEqual && lastInsert > lastDelete) {if (lastDelete > lastEqual) {removeNewLineFromLastDelete = true;removeNewLineFromLastInsert = true;} else if (lastEqual > lastDelete) {removeNewLineFromLastEqual = true;removeNewLineFromLastInsert = true;prependNewLineAfterLastEqual = true;}}}}if (removeNewLineFromLastEqual) {Diff &diff = diffList[lastEqual];diff.text = diff.text.left(diff.text.size() - 1);}if (removeNewLineFromLastDelete) {Diff &diff = diffList[lastDelete];diff.text = diff.text.left(diff.text.size() - 1);}if (removeNewLineFromLastInsert) {Diff &diff = diffList[lastInsert];diff.text = diff.text.left(diff.text.size() - 1);}if (prependNewLineAfterLastEqual) {Diff &diff = diffList[lastEqual + 1];diff.text = newLine + diff.text;}if (lastChunkAtTheEndOfFile) {*lastChunkAtTheEndOfFile = noNewLineInEqual >= 0|| noNewLineInDelete >= 0 || noNewLineInInsert >= 0;}// diffList = Differ::merge(diffList);QList<Diff> leftDiffList;QList<Diff> rightDiffList;Differ::splitDiffList(diffList, &leftDiffList, &rightDiffList);QList<Diff> outputLeftDiffList;QList<Diff> outputRightDiffList;Differ::diffBetweenEqualities(leftDiffList,rightDiffList,&outputLeftDiffList,&outputRightDiffList);return calculateOriginalData(outputLeftDiffList,outputRightDiffList).rows;}bool GitDiffWorker::detectFileData(const QString &patch, FileData *fileData, QString *remainingPatch){bool hasNewLine;QString afterDiffGit;// diff --git a/leftFileName b/rightFileNameconst QString diffGit = readLine(patch, &afterDiffGit, &hasNewLine);if (!hasNewLine)return false; // we need to have at least one more lineconst QLatin1String gitHeader("diff --git ");const QString fileNames = diffGit.mid(gitHeader.size());QString commonFileName;if (extractCommonFileName(fileNames, &commonFileName)) {// change / new / deletefileData->fileOperation = FileData::ChangeFile;fileData->fileInfo[LeftSide].fileName = fileData->fileInfo[RightSide].fileName = commonFileName;QString afterSecondLine;const QString secondLine = readLine(afterDiffGit, &afterSecondLine, &hasNewLine);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 "))) {QString afterThirdLine;// new modereadLine(afterSecondLine, &afterThirdLine, &hasNewLine);if (!hasNewLine)fileData->fileOperation = FileData::ChangeMode;// TODO: validate new mode line*remainingPatch = afterThirdLine;} else {*remainingPatch = afterDiffGit;}} else {// copy / renameQString afterModeOrSimilarity;QString afterSimilarity;const QString secondLine = readLine(afterDiffGit, &afterModeOrSimilarity, &hasNewLine);if (secondLine.startsWith(QLatin1String("old mode "))) {if (!hasNewLine)return false;readLine(afterModeOrSimilarity, &afterModeOrSimilarity, &hasNewLine); // new modeif (!hasNewLine)return false;// (dis)similarity index [0-100]%readLine(afterModeOrSimilarity, &afterSimilarity, &hasNewLine);} else {afterSimilarity = afterModeOrSimilarity;}if (!hasNewLine)return false; // we need to have at least one more lineQString afterCopyRenameFrom;// [copy / rename] from leftFileNameconst QString copyRenameFrom = readLine(afterSimilarity, &afterCopyRenameFrom, &hasNewLine);if (!hasNewLine)return false; // we need to have at least one more lineconst QLatin1String copyFrom("copy from ");const QLatin1String renameFrom("rename from ");if (copyRenameFrom.startsWith(copyFrom)) {fileData->fileOperation = FileData::CopyFile;fileData->fileInfo[LeftSide].fileName = copyRenameFrom.mid(copyFrom.size());} else if (copyRenameFrom.startsWith(renameFrom)) {fileData->fileOperation = FileData::RenameFile;fileData->fileInfo[LeftSide].fileName = copyRenameFrom.mid(renameFrom.size());} else {return false;}QString afterCopyRenameTo;// [copy / rename] to rightFileNameconst QString copyRenameTo = readLine(afterCopyRenameFrom, &afterCopyRenameTo, &hasNewLine);// if (dis)similarity index is 100% we don't have more linesconst QLatin1String copyTo("copy to ");const QLatin1String renameTo("rename to ");if (fileData->fileOperation == FileData::CopyFile && copyRenameTo.startsWith(copyTo)) {fileData->fileInfo[RightSide].fileName = copyRenameTo.mid(copyTo.size());} else if (fileData->fileOperation == FileData::RenameFile && copyRenameTo.startsWith(renameTo)) {fileData->fileInfo[RightSide].fileName = copyRenameTo.mid(renameTo.size());} else {return false;}*remainingPatch = afterCopyRenameTo;}return detectIndexAndBinary(*remainingPatch, fileData, remainingPatch);}bool GitDiffWorker::extractCommonFileName(const QString &fileNames, QString *fileName){// we should have 1 space between filenamesif (fileNames.size() % 2 == 0)return false;if (!fileNames.startsWith(QLatin1String("a/")))return false;// drop the space in betweenconst int fileNameSize = fileNames.size() / 2;if (!fileNames.mid(fileNameSize).startsWith(QLatin1String(" b/")))return false;// drop "a/"const QString leftFileName = fileNames.mid(2, fileNameSize - 2);// drop the first filename + " b/"const QString rightFileName = fileNames.mid(fileNameSize + 3, fileNameSize - 2);if (leftFileName != rightFileName)return false;*fileName = leftFileName;return true;}bool GitDiffWorker::detectIndexAndBinary(const QString &patch, FileData *fileData, QString *remainingPatch){bool hasNewLine;*remainingPatch = patch;if (remainingPatch->isEmpty()) {switch (fileData->fileOperation) {case FileData::CopyFile:case FileData::RenameFile:case FileData::ChangeMode:// in case of 100% similarity we don't have more lines in the patchreturn true;default:break;}}QString afterNextLine;// index [leftIndexSha]..[rightIndexSha] <optionally: octalNumber>const QString nextLine = readLine(patch, &afterNextLine, &hasNewLine);const QLatin1String indexHeader("index ");if (nextLine.startsWith(indexHeader)) {const QString indices = nextLine.mid(indexHeader.size());const int dotsPosition = indices.indexOf(QStringLiteral(".."));if (dotsPosition < 0)return false;fileData->fileInfo[LeftSide].typeInfo = indices.left(dotsPosition);// if there is no space we take the remaining stringconst int spacePosition = indices.indexOf(QChar::Space, dotsPosition + 2);const int length = spacePosition < 0 ? -1 : spacePosition - dotsPosition - 2;fileData->fileInfo[RightSide].typeInfo = indices.mid(dotsPosition + 2, length);*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 filereturn true;}const QString leftFileName = sideFileName(LeftSide, *fileData);const QString rightFileName = sideFileName(RightSide, *fileData);const QString binaryLine = "Binary files "+ leftFileName + " and "+ rightFileName + " differ";if (*remainingPatch == binaryLine) {fileData->binaryFiles = true;*remainingPatch = QString();return true;}const QString leftStart = "--- " + leftFileName;QString afterMinuses;// --- leftFileNameconst QString minuses = readLine(*remainingPatch, &afterMinuses, &hasNewLine);if (!hasNewLine)return false; // we need to have at least one more lineif (!minuses.startsWith(leftStart))return false;const QString rightStart = "+++ " + rightFileName;QString afterPluses;// +++ rightFileNameconst QString pluses = readLine(afterMinuses, &afterPluses, &hasNewLine);if (!hasNewLine)return false; // we need to have at least one more lineif (!pluses.startsWith(rightStart))return false;*remainingPatch = afterPluses;return true;}QString GitDiffWorker::sideFileName(DiffSide side, const FileData &fileData){const FileData::FileOperation operation = side == LeftSide ? FileData::NewFile: FileData::DeleteFile;if (fileData.fileOperation == operation)return "/dev/null";const QString sideMarker = side == LeftSide ? QString("a/") : QString("b/");return sideMarker + fileData.fileInfo[side].fileName;}void GitDiffWorker::handleDifference(const QString &text, QList<TextLineData> *lines, int *lineNumber){const QStringList newLines = text.split('\n');for (int line = 0; line < newLines.size(); ++line) {const int startPos = line > 0 ? -1 : lines->isEmpty() ? 0 : lines->last().text.size();handleLine(newLines, line, lines, lineNumber);const int endPos = line < newLines.size() - 1? -1: lines->isEmpty() ? 0 : lines->last().text.size();if (!lines->isEmpty())lines->last().changedPositions.insert(startPos, endPos);}}void GitDiffWorker::handleLine(const QStringList &newLines, int line, QList<TextLineData> *lines, int *lineNumber){if (line < newLines.size()) {const QString text = newLines.at(line);if (lines->isEmpty() || line > 0) {if (line > 0)++*lineNumber;lines->append(TextLineData(text));} else {lines->last().text += text;}}}bool GitDiffWorker::lastLinesEqual(const QList<TextLineData> &leftLines, const QList<TextLineData> &rightLines){const bool leftLineEqual = leftLines.isEmpty() || leftLines.last().text.isEmpty();const bool rightLineEqual = rightLines.isEmpty() || rightLines.last().text.isEmpty();return leftLineEqual && rightLineEqual;}QList<TextLineData> GitDiffWorker::assemblyRows(const QList<TextLineData> &lines, const QMap<int, int> &lineSpans){QList<TextLineData> data;const int lineCount = lines.size();for (int i = 0; i <= lineCount; i++) {for (int j = 0; j < lineSpans.value(i); j++)data.append(TextLineData(TextLineData::Separator));if (i < lineCount)data.append(lines.at(i));}return data;}SideBySideDiffOutput GitDiffWorker::diffOutput(const DiffEditorInput &input){SideBySideDiffOutput output;const QChar separator = '\n';int blockNumber = 0;std::array<QString, SideCount> diffText {};auto addFileLine = [&](DiffSide side, const FileData &fileData) {output.side[side].selections[blockNumber].append({ input.fileLineFormat });output.side[side].diffData.setFileInfo(blockNumber, fileData.fileInfo[side]);diffText[side] += separator;};auto addChunkLine = [&](DiffSide side, int skippedLines, const QString &contextInfo = {}) {output.side[side].selections[blockNumber].append({ input.chunkLineFormat });output.side[side].diffData.setSkippedLines(blockNumber, skippedLines, contextInfo);diffText[side] += separator;};auto addRowLine = [&](DiffSide side, const RowData &rowData,int *lineNumber, int *lastLineNumber = nullptr) {if (rowData.line[side].textLineType == TextLineData::TextLine) {diffText[side] += rowData.line[side].text;if (lastLineNumber)*lastLineNumber = *lineNumber;++(*lineNumber);output.side[side].diffData.setLineNumber(blockNumber, *lineNumber);} else if (rowData.line[side].textLineType == TextLineData::Separator) {output.side[side].diffData.setSeparator(blockNumber, true);}if (!rowData.equal) {if (rowData.line[side].textLineType == TextLineData::TextLine)output.side[side].selections[blockNumber].append({ input.lineFormat[side] });elseoutput.side[side].selections[blockNumber].append({ input.spanLineFormat });}for (auto it = rowData.line[side].changedPositions.cbegin(),end = rowData.line[side].changedPositions.cend();it != end; ++it) {output.side[side].selections[blockNumber].append({ input.charFormat[side], it.key(), it.value() });}diffText[side] += separator;};auto addSkippedLine = [&](DiffSide side, int skippedLines) {output.side[side].selections[blockNumber].append({ input.chunkLineFormat });output.side[side].diffData.setSkippedLines(blockNumber, skippedLines);diffText[side] += separator;};for (const FileData &contextFileData : input.contextFileData) {diffText = {};output.foldingIndent.insert(blockNumber, 1);addFileLine(LeftSide, contextFileData);addFileLine(RightSide, contextFileData);blockNumber++;int lastLeftLineNumber = -1;if (contextFileData.binaryFiles) {output.foldingIndent.insert(blockNumber, 2);addChunkLine(LeftSide, -2);addChunkLine(RightSide, -2);blockNumber++;} else {for (int j = 0; j < contextFileData.chunks.size(); j++) {const ChunkData &chunkData = contextFileData.chunks.at(j);int leftLineNumber = chunkData.startingLineNumber[LeftSide];int rightLineNumber = chunkData.startingLineNumber[RightSide];if (!chunkData.contextChunk) {const int skippedLines = leftLineNumber - lastLeftLineNumber - 1;if (skippedLines > 0) {output.foldingIndent.insert(blockNumber, 2);addChunkLine(LeftSide, skippedLines, chunkData.contextInfo);addChunkLine(RightSide, skippedLines, chunkData.contextInfo);blockNumber++;}const int rows = chunkData.rows.size();output.side[LeftSide].diffData.diffChunkInfo.setChunkIndex(blockNumber, rows, j);output.side[RightSide].diffData.diffChunkInfo.setChunkIndex(blockNumber, rows, j);for (const RowData &rowData : chunkData.rows) {addRowLine(LeftSide, rowData, &leftLineNumber, &lastLeftLineNumber);addRowLine(RightSide, rowData, &rightLineNumber);blockNumber++;}}if (j == contextFileData.chunks.size() - 1) { // the last chunkint skippedLines = -2;if (chunkData.contextChunk) {// if it's context chunkskippedLines = chunkData.rows.size();} else if (!contextFileData.lastChunkAtTheEndOfFile&& !contextFileData.contextChunksIncluded) {// if not a context chunk and not a chunk at the end of file// and context lines not includedskippedLines = -1; // unknown count skipped by the end of file}if (skippedLines >= -1) {addSkippedLine(LeftSide, skippedLines);addSkippedLine(RightSide, skippedLines);blockNumber++;} // otherwise nothing skipped}}}diffText[LeftSide].replace('\r', ' ');diffText[RightSide].replace('\r', ' ');output.side[LeftSide].diffText += diffText[LeftSide];output.side[RightSide].diffText += diffText[RightSide];if (isStop)return {};}output.side[LeftSide].selections = polishedSelections(output.side[LeftSide].selections);output.side[RightSide].selections = polishedSelections(output.side[RightSide].selections);return output;}int SideDiffData::blockNumberForFileIndex(int fileIndex) const{if (fileIndex < 0 || fileIndex >= diffFileInfo.count())return -1;return std::next(diffFileInfo.constBegin(), fileIndex).key();}int SideDiffData::fileIndexForBlockNumber(int blockNumber) const{int i = -1;for (auto it = diffFileInfo.cbegin(), end = diffFileInfo.cend(); it != end; ++it, ++i) {if (it.key() > blockNumber)break;}return i;}void SideDiffData::setLineNumber(int blockNumber, int lineNumber){const QString lineNumberString = QString::number(lineNumber);blockLineNumbers.insert(blockNumber, lineNumber);lineNumberDigits = qMax(lineNumberDigits, lineNumberString.size());}void SideDiffData::setFileInfo(int blockNumber, const DiffFileInfo &fileInfo){diffFileInfo[blockNumber] = fileInfo;setSeparator(blockNumber, true);}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。