1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "diffutils.h"
27 
28 #include <utils/algorithm.h>
29 #include <utils/differ.h>
30 #include <utils/porting.h>
31 
32 #include <QFutureInterfaceBase>
33 #include <QRegularExpression>
34 #include <QStringList>
35 #include <QTextStream>
36 
37 using namespace Utils;
38 
39 namespace DiffEditor {
40 
selectedRowsCount() const41 int ChunkSelection::selectedRowsCount() const
42 {
43     return Utils::toSet(leftSelection).unite(Utils::toSet(rightSelection)).size();
44 }
45 
assemblyRows(const QList<TextLineData> & lines,const QMap<int,int> & lineSpans)46 static QList<TextLineData> assemblyRows(const QList<TextLineData> &lines,
47                                         const QMap<int, int> &lineSpans)
48 {
49     QList<TextLineData> data;
50 
51     const int lineCount = lines.size();
52     for (int i = 0; i <= lineCount; i++) {
53         for (int j = 0; j < lineSpans.value(i); j++)
54             data.append(TextLineData(TextLineData::Separator));
55         if (i < lineCount)
56             data.append(lines.at(i));
57     }
58     return data;
59 }
60 
lastLinesEqual(const QList<TextLineData> & leftLines,const QList<TextLineData> & rightLines)61 static bool lastLinesEqual(const QList<TextLineData> &leftLines,
62                            const QList<TextLineData> &rightLines)
63 {
64     const bool leftLineEqual = !leftLines.isEmpty()
65             ? leftLines.last().text.isEmpty()
66             : true;
67     const bool rightLineEqual = !rightLines.isEmpty()
68             ? rightLines.last().text.isEmpty()
69             : true;
70     return leftLineEqual && rightLineEqual;
71 }
72 
handleLine(const QStringList & newLines,int line,QList<TextLineData> * lines,int * lineNumber)73 static void handleLine(const QStringList &newLines,
74                        int line,
75                        QList<TextLineData> *lines,
76                        int *lineNumber)
77 {
78     if (line < newLines.size()) {
79         const QString text = newLines.at(line);
80         if (lines->isEmpty() || line > 0) {
81             if (line > 0)
82                 ++*lineNumber;
83             lines->append(TextLineData(text));
84         } else {
85             lines->last().text += text;
86         }
87     }
88 }
89 
handleDifference(const QString & text,QList<TextLineData> * lines,int * lineNumber)90 static void handleDifference(const QString &text,
91                              QList<TextLineData> *lines,
92                              int *lineNumber)
93 {
94     const QStringList newLines = text.split('\n');
95     for (int line = 0; line < newLines.size(); ++line) {
96         const int startPos = line > 0 ? -1 : lines->isEmpty() ? 0 : lines->last().text.size();
97         handleLine(newLines, line, lines, lineNumber);
98         const int endPos = line < newLines.size() - 1
99                                ? -1
100                                : lines->isEmpty() ? 0 : lines->last().text.size();
101         if (!lines->isEmpty())
102             lines->last().changedPositions.insert(startPos, endPos);
103     }
104 }
105 
106 /*
107  * leftDiffList can contain only deletions and equalities,
108  * while rightDiffList can contain only insertions and equalities.
109  * The number of equalities on both lists must be the same.
110 */
calculateOriginalData(const QList<Diff> & leftDiffList,const QList<Diff> & rightDiffList)111 ChunkData DiffUtils::calculateOriginalData(const QList<Diff> &leftDiffList,
112                                            const QList<Diff> &rightDiffList)
113 {
114     int i = 0;
115     int j = 0;
116 
117     QList<TextLineData> leftLines;
118     QList<TextLineData> rightLines;
119 
120     // <line number, span count>
121     QMap<int, int> leftSpans;
122     QMap<int, int> rightSpans;
123     // <left line number, right line number>
124     QMap<int, int> equalLines;
125 
126     int leftLineNumber = 0;
127     int rightLineNumber = 0;
128     int leftLineAligned = -1;
129     int rightLineAligned = -1;
130     bool lastLineEqual = true;
131 
132     while (i <= leftDiffList.size() && j <= rightDiffList.size()) {
133         const Diff leftDiff = i < leftDiffList.size() ? leftDiffList.at(i) : Diff(Diff::Equal);
134         const Diff rightDiff = j < rightDiffList.size() ? rightDiffList.at(j) : Diff(Diff::Equal);
135 
136         if (leftDiff.command == Diff::Delete) {
137             if (j == rightDiffList.size() && lastLineEqual && leftDiff.text.startsWith('\n'))
138                 equalLines.insert(leftLineNumber, rightLineNumber);
139             // process delete
140             handleDifference(leftDiff.text, &leftLines, &leftLineNumber);
141             lastLineEqual = lastLinesEqual(leftLines, rightLines);
142             if (j == rightDiffList.size())
143                 lastLineEqual = false;
144             i++;
145         }
146         if (rightDiff.command == Diff::Insert) {
147             if (i == leftDiffList.size() && lastLineEqual && rightDiff.text.startsWith('\n'))
148                 equalLines.insert(leftLineNumber, rightLineNumber);
149             // process insert
150             handleDifference(rightDiff.text, &rightLines, &rightLineNumber);
151             lastLineEqual = lastLinesEqual(leftLines, rightLines);
152             if (i == leftDiffList.size())
153                 lastLineEqual = false;
154             j++;
155         }
156         if (leftDiff.command == Diff::Equal && rightDiff.command == Diff::Equal) {
157             // process equal
158             const QStringList newLeftLines = leftDiff.text.split('\n');
159             const QStringList newRightLines = rightDiff.text.split('\n');
160 
161             int line = 0;
162 
163             if (i < leftDiffList.size() || j < rightDiffList.size()
164                 || (!leftLines.isEmpty() && !rightLines.isEmpty())) {
165                 while (line < qMax(newLeftLines.size(), newRightLines.size())) {
166                     handleLine(newLeftLines, line, &leftLines, &leftLineNumber);
167                     handleLine(newRightLines, line, &rightLines, &rightLineNumber);
168 
169                     const int commonLineCount = qMin(newLeftLines.size(), newRightLines.size());
170                     if (line < commonLineCount) {
171                         // try to align
172                         const int leftDifference = leftLineNumber - leftLineAligned;
173                         const int rightDifference = rightLineNumber - rightLineAligned;
174 
175                         if (leftDifference && rightDifference) {
176                             bool doAlign = true;
177                             if (line == 0
178                                     && (newLeftLines.at(0).isEmpty()
179                                         || newRightLines.at(0).isEmpty())
180                                     && !lastLineEqual) {
181                                 // omit alignment when first lines of equalities
182                                 // are empty and last generated lines are not equal
183                                 doAlign = false;
184                             }
185 
186                             if (line == commonLineCount - 1) {
187                                 // omit alignment when last lines of equalities are empty
188                                 if (leftLines.last().text.isEmpty()
189                                         || rightLines.last().text.isEmpty())
190                                     doAlign = false;
191 
192                                 // unless it's the last dummy line (don't omit in that case)
193                                 if (i == leftDiffList.size() && j == rightDiffList.size())
194                                     doAlign = true;
195                             }
196 
197                             if (doAlign) {
198                                 // align here
199                                 leftLineAligned = leftLineNumber;
200                                 rightLineAligned = rightLineNumber;
201 
202                                 // insert separators if needed
203                                 if (rightDifference > leftDifference)
204                                     leftSpans.insert(leftLineNumber,
205                                                      rightDifference - leftDifference);
206                                 else if (leftDifference > rightDifference)
207                                     rightSpans.insert(rightLineNumber,
208                                                       leftDifference - rightDifference);
209                             }
210                         }
211                     }
212 
213                     // check if lines are equal
214                     if ((line < commonLineCount - 1)    // before the last common line in equality
215                         || (line == commonLineCount - 1 // or the last common line in equality
216                             && i == leftDiffList.size() // and it's the last iteration
217                             && j == rightDiffList.size())) {
218                         if (line > 0 || lastLineEqual)
219                             equalLines.insert(leftLineNumber, rightLineNumber);
220                     }
221 
222                     if (line > 0)
223                         lastLineEqual = true;
224 
225                     line++;
226                 }
227             }
228             i++;
229             j++;
230         }
231     }
232 
233     QList<TextLineData> leftData = assemblyRows(leftLines,
234                                                 leftSpans);
235     QList<TextLineData> rightData = assemblyRows(rightLines,
236                                                  rightSpans);
237 
238     // fill ending separators
239     for (int i = leftData.size(); i < rightData.size(); i++)
240         leftData.append(TextLineData(TextLineData::Separator));
241     for (int i = rightData.size(); i < leftData.size(); i++)
242         rightData.append(TextLineData(TextLineData::Separator));
243 
244     const int visualLineCount = leftData.size();
245     int leftLine = -1;
246     int rightLine = -1;
247     ChunkData chunkData;
248 
249     for (int i = 0; i < visualLineCount; i++) {
250         const TextLineData &leftTextLine = leftData.at(i);
251         const TextLineData &rightTextLine = rightData.at(i);
252         RowData row(leftTextLine, rightTextLine);
253 
254         if (leftTextLine.textLineType == TextLineData::TextLine)
255             ++leftLine;
256         if (rightTextLine.textLineType == TextLineData::TextLine)
257             ++rightLine;
258         if (equalLines.value(leftLine, -2) == rightLine)
259             row.equal = true;
260 
261         chunkData.rows.append(row);
262     }
263     return chunkData;
264 }
265 
calculateContextData(const ChunkData & originalData,int contextLineCount,int joinChunkThreshold)266 FileData DiffUtils::calculateContextData(const ChunkData &originalData, int contextLineCount,
267                                          int joinChunkThreshold)
268 {
269     if (contextLineCount < 0)
270         return FileData(originalData);
271 
272     FileData fileData;
273     fileData.contextChunksIncluded = true;
274     fileData.lastChunkAtTheEndOfFile = true;
275 
276     QMap<int, bool> hiddenRows;
277     int i = 0;
278     while (i < originalData.rows.size()) {
279         const RowData &row = originalData.rows[i];
280         if (row.equal) {
281             // count how many equal
282             int equalRowStart = i;
283             i++;
284             while (i < originalData.rows.size()) {
285                 const RowData originalRow = originalData.rows.at(i);
286                 if (!originalRow.equal)
287                     break;
288                 i++;
289             }
290             const bool first = equalRowStart == 0; // includes first line?
291             const bool last = i == originalData.rows.size(); // includes last line?
292 
293             const int firstLine = first
294                     ? 0 : equalRowStart + contextLineCount;
295             const int lastLine = last ? originalData.rows.size() : i - contextLineCount;
296 
297             if (firstLine < lastLine - joinChunkThreshold) {
298                 for (int j = firstLine; j < lastLine; j++) {
299                     hiddenRows.insert(j, true);
300                 }
301             }
302         } else {
303             // iterate to the next row
304             i++;
305         }
306     }
307     i = 0;
308     int leftLineNumber = 0;
309     int rightLineNumber = 0;
310     while (i < originalData.rows.size()) {
311         const bool contextChunk = hiddenRows.contains(i);
312         ChunkData chunkData;
313         chunkData.contextChunk = contextChunk;
314         chunkData.leftStartingLineNumber = leftLineNumber;
315         chunkData.rightStartingLineNumber = rightLineNumber;
316         while (i < originalData.rows.size()) {
317             if (contextChunk != hiddenRows.contains(i))
318                 break;
319             RowData rowData = originalData.rows.at(i);
320             chunkData.rows.append(rowData);
321             if (rowData.leftLine.textLineType == TextLineData::TextLine)
322                 ++leftLineNumber;
323             if (rowData.rightLine.textLineType == TextLineData::TextLine)
324                 ++rightLineNumber;
325             ++i;
326         }
327         fileData.chunks.append(chunkData);
328     }
329 
330     return fileData;
331 }
332 
makePatchLine(const QChar & startLineCharacter,const QString & textLine,bool lastChunk,bool lastLine)333 QString DiffUtils::makePatchLine(const QChar &startLineCharacter,
334                           const QString &textLine,
335                           bool lastChunk,
336                           bool lastLine)
337 {
338     QString line;
339 
340     const bool addNoNewline = lastChunk // it's the last chunk in file
341             && lastLine // it's the last row in chunk
342             && !textLine.isEmpty(); // the row is not empty
343 
344     const bool addLine = !lastChunk // not the last chunk in file
345             || !lastLine // not the last row in chunk
346             || addNoNewline; // no addNoNewline case
347 
348     if (addLine) {
349         line = startLineCharacter + textLine + '\n';
350         if (addNoNewline)
351             line += "\\ No newline at end of file\n";
352     }
353 
354     return line;
355 }
356 
makePatch(const ChunkData & chunkData,bool lastChunk)357 QString DiffUtils::makePatch(const ChunkData &chunkData,
358                              bool lastChunk)
359 {
360     if (chunkData.contextChunk)
361         return QString();
362 
363     QString diffText;
364     int leftLineCount = 0;
365     int rightLineCount = 0;
366     QList<TextLineData> leftBuffer, rightBuffer;
367 
368     int rowToBeSplit = -1;
369 
370     if (lastChunk) {
371         // Detect the case when the last equal line is followed by
372         // only separators on left or on right. In that case
373         // the last equal line needs to be split.
374         const int rowCount = chunkData.rows.size();
375         int i = 0;
376         for (i = rowCount; i > 0; i--) {
377             const RowData &rowData = chunkData.rows.at(i - 1);
378             if (rowData.leftLine.textLineType != TextLineData::Separator
379                     || rowData.rightLine.textLineType != TextLineData::TextLine)
380                 break;
381         }
382         const int leftSeparator = i;
383         for (i = rowCount; i > 0; i--) {
384             const RowData &rowData = chunkData.rows.at(i - 1);
385             if (rowData.rightLine.textLineType != TextLineData::Separator
386                     || rowData.leftLine.textLineType != TextLineData::TextLine)
387                 break;
388         }
389         const int rightSeparator = i;
390         const int commonSeparator = qMin(leftSeparator, rightSeparator);
391         if (commonSeparator > 0
392                 && commonSeparator < rowCount
393                 && chunkData.rows.at(commonSeparator - 1).equal)
394             rowToBeSplit = commonSeparator - 1;
395     }
396 
397     for (int i = 0; i <= chunkData.rows.size(); i++) {
398         const RowData &rowData = i < chunkData.rows.size()
399                                      ? chunkData.rows.at(i)
400                                      : RowData(TextLineData(TextLineData::Separator)); // dummy,
401         // ensure we process buffers to the end.
402         // rowData will be equal
403         if (rowData.equal && i != rowToBeSplit) {
404             if (!leftBuffer.isEmpty()) {
405                 for (int j = 0; j < leftBuffer.size(); j++) {
406                     const QString line = makePatchLine('-',
407                                                        leftBuffer.at(j).text,
408                                                        lastChunk,
409                                                        i == chunkData.rows.size()
410                                                            && j == leftBuffer.size() - 1);
411 
412                     if (!line.isEmpty())
413                         ++leftLineCount;
414 
415                     diffText += line;
416                 }
417                 leftBuffer.clear();
418             }
419             if (!rightBuffer.isEmpty()) {
420                 for (int j = 0; j < rightBuffer.size(); j++) {
421                     const QString line = makePatchLine('+',
422                                                        rightBuffer.at(j).text,
423                                                        lastChunk,
424                                                        i == chunkData.rows.size()
425                                                            && j == rightBuffer.size() - 1);
426 
427                     if (!line.isEmpty())
428                         ++rightLineCount;
429 
430                     diffText += line;
431                 }
432                 rightBuffer.clear();
433             }
434             if (i < chunkData.rows.size()) {
435                 const QString line = makePatchLine(' ',
436                                                    rowData.rightLine.text,
437                                                    lastChunk,
438                                                    i == chunkData.rows.size() - 1);
439 
440                 if (!line.isEmpty()) {
441                     ++leftLineCount;
442                     ++rightLineCount;
443                 }
444 
445                 diffText += line;
446             }
447         } else {
448             if (rowData.leftLine.textLineType == TextLineData::TextLine)
449                 leftBuffer.append(rowData.leftLine);
450             if (rowData.rightLine.textLineType == TextLineData::TextLine)
451                 rightBuffer.append(rowData.rightLine);
452         }
453     }
454 
455     const QString chunkLine = "@@ -"
456             + QString::number(chunkData.leftStartingLineNumber + 1)
457             + ','
458             + QString::number(leftLineCount)
459             + " +"
460             + QString::number(chunkData.rightStartingLineNumber + 1)
461             + ','
462             + QString::number(rightLineCount)
463             + " @@"
464             + chunkData.contextInfo
465             + '\n';
466 
467     diffText.prepend(chunkLine);
468 
469     return diffText;
470 }
471 
makePatch(const ChunkData & chunkData,const QString & leftFileName,const QString & rightFileName,bool lastChunk)472 QString DiffUtils::makePatch(const ChunkData &chunkData,
473                              const QString &leftFileName,
474                              const QString &rightFileName,
475                              bool lastChunk)
476 {
477     QString diffText = makePatch(chunkData, lastChunk);
478 
479     const QString rightFileInfo = "+++ " + rightFileName + '\n';
480     const QString leftFileInfo = "--- " + leftFileName + '\n';
481 
482     diffText.prepend(rightFileInfo);
483     diffText.prepend(leftFileInfo);
484 
485     return diffText;
486 }
487 
leftFileName(const FileData & fileData,unsigned formatFlags)488 static QString leftFileName(const FileData &fileData, unsigned formatFlags)
489 {
490     QString diffText;
491     QTextStream str(&diffText);
492     if (fileData.fileOperation == FileData::NewFile) {
493         str << "/dev/null";
494     } else {
495         if (formatFlags & DiffUtils::AddLevel)
496             str << "a/";
497         str << fileData.leftFileInfo.fileName;
498     }
499     return diffText;
500 }
501 
rightFileName(const FileData & fileData,unsigned formatFlags)502 static QString rightFileName(const FileData &fileData, unsigned formatFlags)
503 {
504     QString diffText;
505     QTextStream str(&diffText);
506     if (fileData.fileOperation == FileData::DeleteFile) {
507         str << "/dev/null";
508     } else {
509         if (formatFlags & DiffUtils::AddLevel)
510             str << "b/";
511         str << fileData.rightFileInfo.fileName;
512     }
513     return diffText;
514 }
515 
makePatch(const QList<FileData> & fileDataList,unsigned formatFlags)516 QString DiffUtils::makePatch(const QList<FileData> &fileDataList, unsigned formatFlags)
517 {
518     QString diffText;
519     QTextStream str(&diffText);
520 
521     for (int i = 0; i < fileDataList.size(); i++) {
522         const FileData &fileData = fileDataList.at(i);
523         if (formatFlags & GitFormat) {
524             str << "diff --git a/" << fileData.leftFileInfo.fileName
525                 << " b/" << fileData.rightFileInfo.fileName << '\n';
526         }
527         if (fileData.fileOperation == FileData::NewFile
528                 || fileData.fileOperation == FileData::DeleteFile) { // git only?
529             if (fileData.fileOperation == FileData::NewFile)
530                 str << "new";
531             else
532                 str << "deleted";
533             str << " file mode 100644\n";
534         }
535         str << "index " << fileData.leftFileInfo.typeInfo << ".." << fileData.rightFileInfo.typeInfo;
536         if (fileData.fileOperation == FileData::ChangeFile)
537             str << " 100644";
538         str << "\n";
539 
540         if (fileData.binaryFiles) {
541             str << "Binary files ";
542             str << leftFileName(fileData, formatFlags);
543             str << " and ";
544             str << rightFileName(fileData, formatFlags);
545             str << " differ\n";
546         } else {
547             if (!fileData.chunks.isEmpty()) {
548                 str << "--- ";
549                 str << leftFileName(fileData, formatFlags) << "\n";
550                 str << "+++ ";
551                 str << rightFileName(fileData, formatFlags) << "\n";
552                 for (int j = 0; j < fileData.chunks.size(); j++) {
553                     str << makePatch(fileData.chunks.at(j),
554                                      (j == fileData.chunks.size() - 1)
555                                          && fileData.lastChunkAtTheEndOfFile);
556                 }
557             }
558         }
559     }
560     return diffText;
561 }
562 
readLines(StringView patch,bool lastChunk,bool * lastChunkAtTheEndOfFile,bool * ok)563 static QList<RowData> readLines(StringView patch, bool lastChunk, bool *lastChunkAtTheEndOfFile, bool *ok)
564 {
565     QList<Diff> diffList;
566 
567     const QChar newLine = '\n';
568 
569     int lastEqual = -1;
570     int lastDelete = -1;
571     int lastInsert = -1;
572 
573     int noNewLineInEqual = -1;
574     int noNewLineInDelete = -1;
575     int noNewLineInInsert = -1;
576 
577     const QVector<StringView> lines = patch.split(newLine);
578     int i;
579     for (i = 0; i < lines.size(); i++) {
580         StringView line = lines.at(i);
581         if (line.isEmpty()) { // need to have at least one character (1 column)
582             if (lastChunk)
583                 i = lines.size(); // pretend as we've read all the lines (we just ignore the rest)
584             break;
585         }
586         const QChar firstCharacter = line.at(0);
587         if (firstCharacter == '\\') { // no new line marker
588             if (!lastChunk) // can only appear in last chunk of the file
589                 break;
590             if (!diffList.isEmpty()) {
591                 Diff &last = diffList.last();
592                 if (last.text.isEmpty())
593                     break;
594 
595                 if (last.command == Diff::Equal) {
596                     if (noNewLineInEqual >= 0)
597                         break;
598                     noNewLineInEqual = diffList.size() - 1;
599                 } else if (last.command == Diff::Delete) {
600                     if (noNewLineInDelete >= 0)
601                         break;
602                     noNewLineInDelete = diffList.size() - 1;
603                 } else if (last.command == Diff::Insert) {
604                     if (noNewLineInInsert >= 0)
605                         break;
606                     noNewLineInInsert = diffList.size() - 1;
607                 }
608             }
609         } else {
610             Diff::Command command = Diff::Equal;
611             if (firstCharacter == ' ') { // common line
612                 command = Diff::Equal;
613             } else if (firstCharacter == '-') { // deleted line
614                 command = Diff::Delete;
615             } else if (firstCharacter == '+') { // inserted line
616                 command = Diff::Insert;
617             } else { // no other character may exist as the first character
618                 if (lastChunk)
619                     i = lines.size(); // pretend as we've read all the lines (we just ignore the rest)
620                 break;
621             }
622 
623             Diff diffToBeAdded(command, line.mid(1).toString() + newLine);
624 
625             if (!diffList.isEmpty() && diffList.last().command == command)
626                 diffList.last().text.append(diffToBeAdded.text);
627             else
628                 diffList.append(diffToBeAdded);
629 
630             if (command == Diff::Equal) // common line
631                 lastEqual = diffList.size() - 1;
632             else if (command == Diff::Delete) // deleted line
633                 lastDelete = diffList.size() - 1;
634             else if (command == Diff::Insert) // inserted line
635                 lastInsert = diffList.size() - 1;
636         }
637     }
638 
639     if (i < lines.size() // we broke before
640         // or we have noNewLine in some equal line and in either delete or insert line
641         || (noNewLineInEqual >= 0 && (noNewLineInDelete >= 0 || noNewLineInInsert >= 0))
642         // or we have noNewLine in not the last equal line
643         || (noNewLineInEqual >= 0 && noNewLineInEqual != lastEqual)
644         // or we have noNewLine in not the last delete line or there is a equal line after the noNewLine for delete
645         || (noNewLineInDelete >= 0 && (noNewLineInDelete != lastDelete || lastEqual > lastDelete))
646         // or we have noNewLine in not the last insert line or there is a equal line after the noNewLine for insert
647         || (noNewLineInInsert >= 0 && (noNewLineInInsert != lastInsert || lastEqual > lastInsert))) {
648         if (ok)
649             *ok = false;
650         return QList<RowData>();
651     }
652 
653     if (ok)
654         *ok = true;
655 
656     bool removeNewLineFromLastEqual = false;
657     bool removeNewLineFromLastDelete = false;
658     bool removeNewLineFromLastInsert = false;
659     bool prependNewLineAfterLastEqual = false;
660 
661     if (noNewLineInDelete >= 0 || noNewLineInInsert >= 0) {
662         if (noNewLineInDelete >= 0)
663             removeNewLineFromLastDelete = true;
664         if (noNewLineInInsert >= 0)
665             removeNewLineFromLastInsert = true;
666     } else {
667         if (noNewLineInEqual >= 0) {
668             removeNewLineFromLastEqual = true;
669         } else {
670             if (lastEqual > lastDelete && lastEqual > lastInsert) {
671                 removeNewLineFromLastEqual = true;
672             } else if (lastDelete > lastEqual && lastDelete > lastInsert) {
673                 if (lastInsert > lastEqual) {
674                     removeNewLineFromLastDelete = true;
675                     removeNewLineFromLastInsert = true;
676                 } else if (lastEqual > lastInsert) {
677                     removeNewLineFromLastEqual = true;
678                     removeNewLineFromLastDelete = true;
679                     prependNewLineAfterLastEqual = true;
680                 }
681             } else if (lastInsert > lastEqual && lastInsert > lastDelete) {
682                 if (lastDelete > lastEqual) {
683                     removeNewLineFromLastDelete = true;
684                     removeNewLineFromLastInsert = true;
685                 } else if (lastEqual > lastDelete) {
686                     removeNewLineFromLastEqual = true;
687                     removeNewLineFromLastInsert = true;
688                     prependNewLineAfterLastEqual = true;
689                 }
690             }
691         }
692     }
693 
694 
695     if (removeNewLineFromLastEqual) {
696         Diff &diff = diffList[lastEqual];
697         diff.text = diff.text.left(diff.text.size() - 1);
698     }
699     if (removeNewLineFromLastDelete) {
700         Diff &diff = diffList[lastDelete];
701         diff.text = diff.text.left(diff.text.size() - 1);
702     }
703     if (removeNewLineFromLastInsert) {
704         Diff &diff = diffList[lastInsert];
705         diff.text = diff.text.left(diff.text.size() - 1);
706     }
707     if (prependNewLineAfterLastEqual) {
708         Diff &diff = diffList[lastEqual + 1];
709         diff.text = newLine + diff.text;
710     }
711 
712     if (lastChunkAtTheEndOfFile) {
713         *lastChunkAtTheEndOfFile = noNewLineInEqual >= 0
714                 || noNewLineInDelete >= 0|| noNewLineInInsert >= 0;
715     }
716 
717 //    diffList = Differ::merge(diffList);
718     QList<Diff> leftDiffList;
719     QList<Diff> rightDiffList;
720     Differ::splitDiffList(diffList, &leftDiffList, &rightDiffList);
721     QList<Diff> outputLeftDiffList;
722     QList<Diff> outputRightDiffList;
723 
724     Differ::diffBetweenEqualities(leftDiffList,
725                                   rightDiffList,
726                                   &outputLeftDiffList,
727                                   &outputRightDiffList);
728 
729     return DiffUtils::calculateOriginalData(outputLeftDiffList,
730                                             outputRightDiffList).rows;
731 }
732 
readLine(StringView text,StringView * remainingText,bool * hasNewLine)733 static StringView readLine(StringView text, StringView *remainingText, bool *hasNewLine)
734 {
735     const QChar newLine('\n');
736     const int indexOfFirstNewLine = text.indexOf(newLine);
737     if (indexOfFirstNewLine < 0) {
738         if (remainingText)
739             *remainingText = StringView();
740         if (hasNewLine)
741             *hasNewLine = false;
742         return text;
743     }
744 
745     if (hasNewLine)
746         *hasNewLine = true;
747 
748     if (remainingText)
749         *remainingText = text.mid(indexOfFirstNewLine + 1);
750 
751     return text.left(indexOfFirstNewLine);
752 }
753 
detectChunkData(StringView chunkDiff,ChunkData * chunkData,StringView * remainingPatch)754 static bool detectChunkData(StringView chunkDiff, ChunkData *chunkData, StringView *remainingPatch)
755 {
756     bool hasNewLine;
757     const StringView chunkLine = readLine(chunkDiff, remainingPatch, &hasNewLine);
758 
759     const QLatin1String leftPosMarker("@@ -");
760     const QLatin1String rightPosMarker(" +");
761     const QLatin1String optionalHintMarker(" @@");
762 
763     const int leftPosIndex = chunkLine.indexOf(leftPosMarker);
764     if (leftPosIndex != 0)
765         return false;
766 
767     const int rightPosIndex = chunkLine.indexOf(rightPosMarker, leftPosIndex + leftPosMarker.size());
768     if (rightPosIndex < 0)
769         return false;
770 
771     const int optionalHintIndex = chunkLine.indexOf(optionalHintMarker, rightPosIndex + rightPosMarker.size());
772     if (optionalHintIndex < 0)
773         return false;
774 
775     const int leftPosStart = leftPosIndex + leftPosMarker.size();
776     const int leftPosLength = rightPosIndex - leftPosStart;
777     StringView leftPos = chunkLine.mid(leftPosStart, leftPosLength);
778 
779     const int rightPosStart = rightPosIndex + rightPosMarker.size();
780     const int rightPosLength = optionalHintIndex - rightPosStart;
781     StringView rightPos = chunkLine.mid(rightPosStart, rightPosLength);
782 
783     const int optionalHintStart = optionalHintIndex + optionalHintMarker.size();
784     const int optionalHintLength = chunkLine.size() - optionalHintStart;
785     const StringView optionalHint = chunkLine.mid(optionalHintStart, optionalHintLength);
786 
787     const QChar comma(',');
788     bool ok;
789 
790     const int leftCommaIndex = leftPos.indexOf(comma);
791     if (leftCommaIndex >= 0)
792         leftPos = leftPos.left(leftCommaIndex);
793     const int leftLineNumber = leftPos.toString().toInt(&ok);
794     if (!ok)
795         return false;
796 
797     const int rightCommaIndex = rightPos.indexOf(comma);
798     if (rightCommaIndex >= 0)
799         rightPos = rightPos.left(rightCommaIndex);
800     const int rightLineNumber = rightPos.toString().toInt(&ok);
801     if (!ok)
802         return false;
803 
804     chunkData->leftStartingLineNumber = leftLineNumber - 1;
805     chunkData->rightStartingLineNumber = rightLineNumber - 1;
806     chunkData->contextInfo = optionalHint.toString();
807 
808     return true;
809 }
810 
readChunks(StringView patch,bool * lastChunkAtTheEndOfFile,bool * ok)811 static QList<ChunkData> readChunks(StringView patch, bool *lastChunkAtTheEndOfFile, bool *ok)
812 {
813     QList<ChunkData> chunkDataList;
814     int position = -1;
815 
816     QVector<int> startingPositions; // store starting positions of @@
817     if (patch.startsWith(QStringLiteral("@@ -")))
818         startingPositions.append(position + 1);
819 
820     while ((position = patch.indexOf(QStringLiteral("\n@@ -"), position + 1)) >= 0)
821         startingPositions.append(position + 1);
822 
823     const QChar newLine('\n');
824     bool readOk = true;
825 
826     const int count = startingPositions.size();
827     for (int i = 0; i < count; i++) {
828         const int chunkStart = startingPositions.at(i);
829         const int chunkEnd = (i < count - 1)
830                                  // drop the newline before the next chunk start
831                                  ? startingPositions.at(i + 1) - 1
832                                  // drop the possible newline by the end of patch
833                                  : (patch.at(patch.size() - 1) == newLine ? patch.size() - 1
834                                                                           : patch.size());
835 
836         // extract just one chunk
837         const StringView chunkDiff = patch.mid(chunkStart, chunkEnd - chunkStart);
838 
839         ChunkData chunkData;
840         StringView lines;
841         readOk = detectChunkData(chunkDiff, &chunkData, &lines);
842 
843         if (!readOk)
844             break;
845 
846         chunkData.rows = readLines(lines, i == (startingPositions.size() - 1),
847                                    lastChunkAtTheEndOfFile, &readOk);
848         if (!readOk)
849             break;
850 
851         chunkDataList.append(chunkData);
852     }
853 
854     if (ok)
855         *ok = readOk;
856 
857     return chunkDataList;
858 }
859 
readDiffHeaderAndChunks(StringView headerAndChunks,bool * ok)860 static FileData readDiffHeaderAndChunks(StringView headerAndChunks, bool *ok)
861 {
862     StringView patch = headerAndChunks;
863     FileData fileData;
864     bool readOk = false;
865 
866     const QRegularExpression leftFileRegExp(
867           "(?:\\n|^)-{3} "       // "--- "
868           "([^\\t\\n]+)"         // "fileName1"
869           "(?:\\t[^\\n]*)*\\n"); // optionally followed by: \t anything \t anything ...)
870     const QRegularExpression rightFileRegExp(
871           "^\\+{3} "             // "+++ "
872           "([^\\t\\n]+)"         // "fileName2"
873           "(?:\\t[^\\n]*)*\\n"); // optionally followed by: \t anything \t anything ...)
874     const QRegularExpression binaryRegExp(
875           "^Binary files ([^\\t\\n]+) and ([^\\t\\n]+) differ$");
876 
877     // followed either by leftFileRegExp
878     const QRegularExpressionMatch leftMatch = leftFileRegExp.match(patch);
879     if (leftMatch.hasMatch() && leftMatch.capturedStart() == 0) {
880         patch = patch.mid(leftMatch.capturedEnd());
881         fileData.leftFileInfo.fileName = leftMatch.captured(1);
882 
883         // followed by rightFileRegExp
884         const QRegularExpressionMatch rightMatch = rightFileRegExp.match(patch);
885         if (rightMatch.hasMatch() && rightMatch.capturedStart() == 0) {
886             patch = patch.mid(rightMatch.capturedEnd());
887             fileData.rightFileInfo.fileName = rightMatch.captured(1);
888 
889             fileData.chunks = readChunks(patch,
890                                          &fileData.lastChunkAtTheEndOfFile,
891                                          &readOk);
892         }
893     } else {
894         // or by binaryRegExp
895         const QRegularExpressionMatch binaryMatch = binaryRegExp.match(patch);
896         if (binaryMatch.hasMatch() && binaryMatch.capturedStart() == 0) {
897             fileData.leftFileInfo.fileName = binaryMatch.captured(1);
898             fileData.rightFileInfo.fileName = binaryMatch.captured(2);
899             fileData.binaryFiles = true;
900             readOk = true;
901         }
902     }
903 
904     if (ok)
905         *ok = readOk;
906 
907     if (!readOk)
908         return FileData();
909 
910     return fileData;
911 
912 }
913 
readDiffPatch(StringView patch,bool * ok,QFutureInterfaceBase * jobController)914 static QList<FileData> readDiffPatch(StringView patch, bool *ok, QFutureInterfaceBase *jobController)
915 {
916     const QRegularExpression diffRegExp("(?:\\n|^)"          // new line of the beginning of a patch
917                                         "("                  // either
918                                         "-{3} "              // ---
919                                         "[^\\t\\n]+"         // filename1
920                                         "(?:\\t[^\\n]*)*\\n" // optionally followed by: \t anything \t anything ...
921                                         "\\+{3} "            // +++
922                                         "[^\\t\\n]+"         // filename2
923                                         "(?:\\t[^\\n]*)*\\n" // optionally followed by: \t anything \t anything ...
924                                         "|"                  // or
925                                         "Binary files "
926                                         "[^\\t\\n]+"         // filename1
927                                         " and "
928                                         "[^\\t\\n]+"         // filename2
929                                         " differ"
930                                         ")");                // end of or
931 
932     bool readOk = false;
933 
934     QList<FileData> fileDataList;
935 
936     QRegularExpressionMatch diffMatch = diffRegExp.match(patch);
937     if (diffMatch.hasMatch()) {
938         readOk = true;
939         int lastPos = -1;
940         do {
941             if (jobController && jobController->isCanceled())
942                 return QList<FileData>();
943 
944             int pos = diffMatch.capturedStart();
945             if (lastPos >= 0) {
946                 StringView headerAndChunks = patch.mid(lastPos, pos - lastPos);
947 
948                 const FileData fileData = readDiffHeaderAndChunks(headerAndChunks,
949                                                                   &readOk);
950 
951                 if (!readOk)
952                     break;
953 
954                 fileDataList.append(fileData);
955             }
956             lastPos = pos;
957             pos = diffMatch.capturedEnd();
958             diffMatch = diffRegExp.match(patch, pos);
959         } while (diffMatch.hasMatch());
960 
961         if (readOk) {
962             StringView headerAndChunks = patch.mid(lastPos, patch.size() - lastPos - 1);
963 
964             const FileData fileData = readDiffHeaderAndChunks(headerAndChunks,
965                                                               &readOk);
966 
967             if (readOk)
968                 fileDataList.append(fileData);
969         }
970     }
971 
972     if (ok)
973         *ok = readOk;
974 
975     if (!readOk)
976         return QList<FileData>();
977 
978     return fileDataList;
979 }
980 
981 // The git diff patch format (ChangeFile, NewFile, DeleteFile)
982 // 0.  <some text lines to skip, e.g. show description>\n
983 // 1.  diff --git a/[fileName] b/[fileName]\n
984 // 2a. new file mode [fileModeNumber]\n
985 // 2b. deleted file mode [fileModeNumber]\n
986 // 2c. old mode [oldFileModeNumber]\n
987 //     new mode [newFileModeNumber]\n
988 // 2d. <Nothing, only in case of ChangeFile>
989 // 3a.  index [leftIndexSha]..[rightIndexSha] <optionally: octalNumber>
990 // 3b. <Nothing, only in case of ChangeFile, "Dirty submodule" case>
991 // 4a. <Nothing more, only possible in case of NewFile or DeleteFile> ???
992 // 4b. \nBinary files [leftFileNameOrDevNull] and [rightFileNameOrDevNull] differ
993 // 4c. --- [leftFileNameOrDevNull]\n
994 //     +++ [rightFileNameOrDevNull]\n
995 //     <Chunks>
996 
997 // The git diff patch format (CopyFile, RenameFile)
998 // 0.  [some text lines to skip, e.g. show description]\n
999 // 1.  diff --git a/[leftFileName] b/[rightFileName]\n
1000 // 2.  [dis]similarity index [0-100]%\n
1001 //     [copy / rename] from [leftFileName]\n
1002 //     [copy / rename] to [rightFileName]
1003 // 3a. <Nothing more, only when similarity index was 100%>
1004 // 3b. index [leftIndexSha]..[rightIndexSha] <optionally: octalNumber>
1005 // 4.  --- [leftFileNameOrDevNull]\n
1006 //     +++ [rightFileNameOrDevNull]\n
1007 //     <Chunks>
1008 
detectIndexAndBinary(StringView patch,FileData * fileData,StringView * remainingPatch)1009 static bool detectIndexAndBinary(StringView patch, FileData *fileData, StringView *remainingPatch)
1010 {
1011     bool hasNewLine;
1012     *remainingPatch = patch;
1013 
1014     if (remainingPatch->isEmpty()) {
1015         switch (fileData->fileOperation) {
1016         case FileData::CopyFile:
1017         case FileData::RenameFile:
1018         case FileData::ChangeMode:
1019             // in case of 100% similarity we don't have more lines in the patch
1020             return true;
1021         default:
1022             break;
1023         }
1024     }
1025 
1026     StringView afterNextLine;
1027     // index [leftIndexSha]..[rightIndexSha] <optionally: octalNumber>
1028     const StringView nextLine = readLine(patch, &afterNextLine, &hasNewLine);
1029 
1030     const QLatin1String indexHeader("index ");
1031 
1032     if (nextLine.startsWith(indexHeader)) {
1033         const StringView indices = nextLine.mid(indexHeader.size());
1034         const int dotsPosition = indices.indexOf(QStringLiteral(".."));
1035         if (dotsPosition < 0)
1036             return false;
1037         fileData->leftFileInfo.typeInfo = indices.left(dotsPosition).toString();
1038 
1039         // if there is no space we take the remaining string
1040         const int spacePosition = indices.indexOf(QChar::Space, dotsPosition + 2);
1041         const int length = spacePosition < 0 ? -1 : spacePosition - dotsPosition - 2;
1042         fileData->rightFileInfo.typeInfo = indices.mid(dotsPosition + 2, length).toString();
1043 
1044         *remainingPatch = afterNextLine;
1045     } else if (fileData->fileOperation != FileData::ChangeFile) {
1046         // no index only in case of ChangeFile,
1047         // the dirty submodule diff case, see "Dirty Submodule" test:
1048         return false;
1049     }
1050 
1051     if (remainingPatch->isEmpty() && (fileData->fileOperation == FileData::NewFile
1052                             || fileData->fileOperation == FileData::DeleteFile)) {
1053         // OK in case of empty file
1054         return true;
1055     }
1056 
1057     const QString devNull("/dev/null");
1058     const QString leftFileName = fileData->fileOperation == FileData::NewFile
1059             ? devNull : QLatin1String("a/") + fileData->leftFileInfo.fileName;
1060     const QString rightFileName = fileData->fileOperation == FileData::DeleteFile
1061             ? devNull : QLatin1String("b/") + fileData->rightFileInfo.fileName;
1062 
1063     const QString binaryLine = "Binary files "
1064             + leftFileName + " and "
1065             + rightFileName + " differ";
1066 
1067     if (*remainingPatch == binaryLine) {
1068         fileData->binaryFiles = true;
1069         *remainingPatch = StringView();
1070         return true;
1071     }
1072 
1073     const QString leftStart = "--- " + leftFileName;
1074     StringView afterMinuses;
1075     // --- leftFileName
1076     const StringView minuses = readLine(*remainingPatch, &afterMinuses, &hasNewLine);
1077     if (!hasNewLine)
1078         return false; // we need to have at least one more line
1079 
1080     if (!minuses.startsWith(leftStart))
1081         return false;
1082 
1083     const QString rightStart = "+++ " + rightFileName;
1084     StringView afterPluses;
1085     // +++ rightFileName
1086     const StringView pluses = readLine(afterMinuses, &afterPluses, &hasNewLine);
1087     if (!hasNewLine)
1088         return false; // we need to have at least one more line
1089 
1090     if (!pluses.startsWith(rightStart))
1091         return false;
1092 
1093     *remainingPatch = afterPluses;
1094     return true;
1095 }
1096 
extractCommonFileName(StringView fileNames,StringView * fileName)1097 static bool extractCommonFileName(StringView fileNames, StringView *fileName)
1098 {
1099     // we should have 1 space between filenames
1100     if (fileNames.size() % 2 == 0)
1101         return false;
1102 
1103     if (!fileNames.startsWith(QLatin1String("a/")))
1104         return false;
1105 
1106     // drop the space in between
1107     const int fileNameSize = fileNames.size() / 2;
1108     if (!fileNames.mid(fileNameSize).startsWith(QLatin1String(" b/")))
1109         return false;
1110 
1111     // drop "a/"
1112     const StringView leftFileName = fileNames.mid(2, fileNameSize - 2);
1113 
1114     // drop the first filename + " b/"
1115     const StringView rightFileName = fileNames.mid(fileNameSize + 3, fileNameSize - 2);
1116 
1117     if (leftFileName != rightFileName)
1118         return false;
1119 
1120     *fileName = leftFileName;
1121     return true;
1122 }
1123 
detectFileData(StringView patch,FileData * fileData,StringView * remainingPatch)1124 static bool detectFileData(StringView patch, FileData *fileData, StringView *remainingPatch)
1125 {
1126     bool hasNewLine;
1127 
1128     StringView afterDiffGit;
1129     // diff --git a/leftFileName b/rightFileName
1130     const StringView diffGit = readLine(patch, &afterDiffGit, &hasNewLine);
1131     if (!hasNewLine)
1132         return false; // we need to have at least one more line
1133 
1134     const QLatin1String gitHeader("diff --git ");
1135     const StringView fileNames = diffGit.mid(gitHeader.size());
1136     StringView commonFileName;
1137     if (extractCommonFileName(fileNames, &commonFileName)) {
1138         // change / new / delete
1139 
1140         fileData->fileOperation = FileData::ChangeFile;
1141         fileData->leftFileInfo.fileName = fileData->rightFileInfo.fileName = commonFileName.toString();
1142 
1143         StringView afterSecondLine;
1144         const StringView secondLine = readLine(afterDiffGit, &afterSecondLine, &hasNewLine);
1145 
1146         if (secondLine.startsWith(QStringLiteral("new file mode "))) {
1147             fileData->fileOperation = FileData::NewFile;
1148             *remainingPatch = afterSecondLine;
1149         } else if (secondLine.startsWith(QStringLiteral("deleted file mode "))) {
1150             fileData->fileOperation = FileData::DeleteFile;
1151             *remainingPatch = afterSecondLine;
1152         } else if (secondLine.startsWith(QStringLiteral("old mode "))) {
1153             StringView afterThirdLine;
1154             // new mode
1155             readLine(afterSecondLine, &afterThirdLine, &hasNewLine);
1156             if (!hasNewLine)
1157                 fileData->fileOperation = FileData::ChangeMode;
1158 
1159             // TODO: validate new mode line
1160             *remainingPatch = afterThirdLine;
1161         } else {
1162             *remainingPatch = afterDiffGit;
1163         }
1164 
1165     } else {
1166         // copy / rename
1167 
1168         StringView afterSimilarity;
1169         // (dis)similarity index [0-100]%
1170         readLine(afterDiffGit, &afterSimilarity, &hasNewLine);
1171         if (!hasNewLine)
1172             return false; // we need to have at least one more line
1173 
1174         // TODO: validate similarity line
1175 
1176         StringView afterCopyRenameFrom;
1177         // [copy / rename] from leftFileName
1178         const StringView copyRenameFrom = readLine(afterSimilarity, &afterCopyRenameFrom, &hasNewLine);
1179         if (!hasNewLine)
1180             return false; // we need to have at least one more line
1181 
1182         const QLatin1String copyFrom("copy from ");
1183         const QLatin1String renameFrom("rename from ");
1184         if (copyRenameFrom.startsWith(copyFrom)) {
1185             fileData->fileOperation = FileData::CopyFile;
1186             fileData->leftFileInfo.fileName = copyRenameFrom.mid(copyFrom.size()).toString();
1187         } else if (copyRenameFrom.startsWith(renameFrom)) {
1188             fileData->fileOperation = FileData::RenameFile;
1189             fileData->leftFileInfo.fileName = copyRenameFrom.mid(renameFrom.size()).toString();
1190         } else {
1191             return false;
1192         }
1193 
1194         StringView afterCopyRenameTo;
1195         // [copy / rename] to rightFileName
1196         const StringView copyRenameTo = readLine(afterCopyRenameFrom, &afterCopyRenameTo, &hasNewLine);
1197 
1198         // if (dis)similarity index is 100% we don't have more lines
1199 
1200         const QLatin1String copyTo("copy to ");
1201         const QLatin1String renameTo("rename to ");
1202         if (fileData->fileOperation == FileData::CopyFile && copyRenameTo.startsWith(copyTo)) {
1203             fileData->rightFileInfo.fileName = copyRenameTo.mid(copyTo.size()).toString();
1204         } else if (fileData->fileOperation == FileData::RenameFile && copyRenameTo.startsWith(renameTo)) {
1205             fileData->rightFileInfo.fileName = copyRenameTo.mid(renameTo.size()).toString();
1206         } else {
1207             return false;
1208         }
1209 
1210         *remainingPatch = afterCopyRenameTo;
1211     }
1212     return detectIndexAndBinary(*remainingPatch, fileData, remainingPatch);
1213 }
1214 
readGitPatch(StringView patch,bool * ok,QFutureInterfaceBase * jobController)1215 static QList<FileData> readGitPatch(StringView patch, bool *ok, QFutureInterfaceBase *jobController)
1216 {
1217     int position = -1;
1218 
1219     QVector<int> startingPositions; // store starting positions of git headers
1220     if (patch.startsWith(QStringLiteral("diff --git ")))
1221         startingPositions.append(position + 1);
1222 
1223     while ((position = patch.indexOf(QStringLiteral("\ndiff --git "), position + 1)) >= 0)
1224         startingPositions.append(position + 1);
1225 
1226     class PatchInfo {
1227     public:
1228         StringView patch;
1229         FileData fileData;
1230     };
1231 
1232     const QChar newLine('\n');
1233     bool readOk = true;
1234 
1235     QVector<PatchInfo> patches;
1236     const int count = startingPositions.size();
1237     for (int i = 0; i < count; i++) {
1238         if (jobController && jobController->isCanceled())
1239             return QList<FileData>();
1240 
1241         const int diffStart = startingPositions.at(i);
1242         const int diffEnd = (i < count - 1)
1243                                 // drop the newline before the next header start
1244                                 ? startingPositions.at(i + 1) - 1
1245                                 // drop the possible newline by the end of file
1246                                 : (patch.at(patch.size() - 1) == newLine ? patch.size() - 1
1247                                                                          : patch.size());
1248 
1249         // extract the patch for just one file
1250         const StringView fileDiff = patch.mid(diffStart, diffEnd - diffStart);
1251 
1252         FileData fileData;
1253         StringView remainingFileDiff;
1254         readOk = detectFileData(fileDiff, &fileData, &remainingFileDiff);
1255 
1256         if (!readOk)
1257             break;
1258 
1259         patches.append(PatchInfo { remainingFileDiff, fileData });
1260     }
1261 
1262     if (!readOk) {
1263         if (ok)
1264             *ok = readOk;
1265         return QList<FileData>();
1266     }
1267 
1268     if (jobController)
1269         jobController->setProgressRange(0, patches.size());
1270 
1271     QList<FileData> fileDataList;
1272     readOk = false;
1273     int i = 0;
1274     for (const auto &patchInfo : qAsConst(patches)) {
1275         if (jobController) {
1276             if (jobController->isCanceled())
1277                 return QList<FileData>();
1278             jobController->setProgressValue(i++);
1279         }
1280 
1281         FileData fileData = patchInfo.fileData;
1282         if (!patchInfo.patch.isEmpty() || fileData.fileOperation == FileData::ChangeFile)
1283             fileData.chunks = readChunks(patchInfo.patch, &fileData.lastChunkAtTheEndOfFile, &readOk);
1284         else
1285             readOk = true;
1286 
1287         if (!readOk)
1288             break;
1289 
1290         fileDataList.append(fileData);
1291     }
1292 
1293     if (ok)
1294         *ok = readOk;
1295 
1296     if (!readOk)
1297         return QList<FileData>();
1298 
1299     return fileDataList;
1300 }
1301 
readPatch(const QString & patch,bool * ok,QFutureInterfaceBase * jobController)1302 QList<FileData> DiffUtils::readPatch(const QString &patch, bool *ok,
1303                                      QFutureInterfaceBase *jobController)
1304 {
1305     bool readOk = false;
1306 
1307     QList<FileData> fileDataList;
1308 
1309     if (jobController) {
1310         jobController->setProgressRange(0, 1);
1311         jobController->setProgressValue(0);
1312     }
1313     StringView croppedPatch = make_stringview(patch);
1314     // Crop e.g. "-- \n2.10.2.windows.1\n\n" at end of file
1315     const QRegularExpression formatPatchEndingRegExp("(\\n-- \\n\\S*\\n\\n$)");
1316     const QRegularExpressionMatch match = formatPatchEndingRegExp.match(croppedPatch);
1317     if (match.hasMatch())
1318         croppedPatch = croppedPatch.left(match.capturedStart() + 1);
1319 
1320     fileDataList = readGitPatch(croppedPatch, &readOk, jobController);
1321     if (!readOk)
1322         fileDataList = readDiffPatch(croppedPatch, &readOk, jobController);
1323 
1324     if (ok)
1325         *ok = readOk;
1326 
1327     return fileDataList;
1328 }
1329 
1330 } // namespace DiffEditor
1331