File indexing completed on 2024-12-01 05:11:57

0001 // clang-format off
0002 /*
0003  * KDiff3 - Text Diff And Merge Tool
0004  *
0005  * SPDX-FileCopyrightText: 2002-2011 Joachim Eibl, joachim.eibl at gmx.de
0006  * SPDX-FileCopyrightText: 2018-2020 Michael Reeves reeves.87@gmail.com
0007  * SPDX-License-Identifier: GPL-2.0-or-later
0008  */
0009 // clang-format on
0010 
0011 #include "MergeFileInfos.h"
0012 
0013 #include "DirectoryInfo.h"
0014 #include "directorymergewindow.h"
0015 #include "fileaccess.h"
0016 #include "Logging.h"
0017 #include "progress.h"
0018 
0019 #include <map>
0020 #include <vector>
0021 
0022 #include <QString>
0023 
0024 #include <KLocalizedString>
0025 
0026 extern std::unique_ptr<Options> gOptions;
0027 
0028 MergeFileInfos::MergeFileInfos() = default;
0029 
0030 MergeFileInfos::~MergeFileInfos()
0031 {
0032     m_children.clear();
0033 }
0034 
0035 void MergeFileInfos::updateParents()
0036 {
0037     MergeFileInfos* current = parent();
0038     while(current != nullptr)
0039     {
0040         bool bChange = false;
0041         if(!isEqualAB() && current->isEqualAB())
0042         {
0043             current->m_bEqualAB = false;
0044             bChange = true;
0045         }
0046         if(!isEqualAC() && current->isEqualAC())
0047         {
0048             current->m_bEqualAC = false;
0049             bChange = true;
0050         }
0051         if(!isEqualBC() && current->isEqualBC())
0052         {
0053             current->m_bEqualBC = false;
0054             bChange = true;
0055         }
0056 
0057         if(bChange)
0058             current->updateAge();
0059         else
0060             break;
0061 
0062         current = current->parent();
0063     }
0064 }
0065 /*
0066     Check for directories or links marked as not equal and mark them equal.
0067 */
0068 void MergeFileInfos::updateDirectoryOrLink()
0069 {
0070     bool bChange = false;
0071     if(!isEqualAB() && isDirA() == isDirB() && isLinkA() == isLinkB())
0072     {
0073         m_bEqualAB = true;
0074         bChange = true;
0075     }
0076     if(!isEqualBC() && isDirC() == isDirB() && isLinkC() == isLinkB())
0077     {
0078         m_bEqualBC = true;
0079         bChange = true;
0080     }
0081     if(!isEqualAC() && isDirA() == isDirC() && isLinkA() == isLinkC())
0082     {
0083         m_bEqualAC = true;
0084         bChange = true;
0085     }
0086 
0087     if(bChange)
0088         updateAge();
0089 }
0090 
0091 QString MergeFileInfos::subPath() const
0092 {
0093     if(m_pFileInfoA != nullptr && m_pFileInfoA->exists())
0094         return m_pFileInfoA->fileRelPath();
0095     else if(m_pFileInfoB != nullptr && m_pFileInfoB->exists())
0096         return m_pFileInfoB->fileRelPath();
0097     else if(m_pFileInfoC != nullptr && m_pFileInfoC->exists())
0098         return m_pFileInfoC->fileRelPath();
0099     return QString("");
0100 }
0101 
0102 QString MergeFileInfos::fileName() const
0103 {
0104     if(m_pFileInfoA != nullptr && m_pFileInfoA->exists())
0105         return m_pFileInfoA->fileName();
0106     else if(m_pFileInfoB != nullptr && m_pFileInfoB->exists())
0107         return m_pFileInfoB->fileName();
0108     else if(m_pFileInfoC != nullptr && m_pFileInfoC->exists())
0109         return m_pFileInfoC->fileName();
0110     return QString("");
0111 }
0112 
0113 bool MergeFileInfos::conflictingFileTypes() const
0114 {
0115     if((m_pFileInfoA != nullptr && !m_pFileInfoA->isNormal()) || (m_pFileInfoB != nullptr && !m_pFileInfoB->isNormal()) || (m_pFileInfoC != nullptr && !m_pFileInfoC->isNormal()))
0116         return true;
0117     // Now check if file/dir-types fit.
0118     if(isLinkA() || isLinkB() || isLinkC())
0119     {
0120         if((existsInA() && !isLinkA()) ||
0121            (existsInB() && !isLinkB()) ||
0122            (existsInC() && !isLinkC()))
0123         {
0124             return true;
0125         }
0126     }
0127 
0128     if(isDirA() || isDirB() || isDirC())
0129     {
0130         if((existsInA() && !isDirA()) ||
0131            (existsInB() && !isDirB()) ||
0132            (existsInC() && !isDirC()))
0133         {
0134             return true;
0135         }
0136     }
0137     return false;
0138 }
0139 
0140 QString MergeFileInfos::fullNameA() const
0141 {
0142     if(existsInA())
0143         return getFileInfoA()->absoluteFilePath();
0144 
0145     return gDirInfo->dirA().absoluteFilePath() + '/' + subPath();
0146 }
0147 
0148 QString MergeFileInfos::fullNameB() const
0149 {
0150     if(existsInB())
0151         return getFileInfoB()->absoluteFilePath();
0152 
0153     return gDirInfo->dirB().absoluteFilePath() + '/' + subPath();
0154 }
0155 
0156 QString MergeFileInfos::fullNameC() const
0157 {
0158     if(existsInC())
0159         return getFileInfoC()->absoluteFilePath();
0160 
0161     return gDirInfo->dirC().absoluteFilePath() + '/' + subPath();
0162 }
0163 
0164 void MergeFileInfos::sort(Qt::SortOrder order)
0165 {
0166     std::sort(m_children.begin(), m_children.end(), MfiCompare(order));
0167 
0168     for(qint32 i = 0; i < m_children.count(); ++i)
0169         m_children[i]->sort(order);
0170 }
0171 
0172 QString MergeFileInfos::fullNameDest() const
0173 {
0174     if(gDirInfo->destDir().prettyAbsPath() == gDirInfo->dirC().prettyAbsPath())
0175         return fullNameC();
0176     else if(gDirInfo->destDir().prettyAbsPath() == gDirInfo->dirB().prettyAbsPath())
0177         return fullNameB();
0178     else
0179         return gDirInfo->destDir().absoluteFilePath() + '/' + subPath();
0180 }
0181 
0182 bool MergeFileInfos::compareFilesAndCalcAges(QStringList& errors, DirectoryMergeWindow* pDMW)
0183 {
0184     enum class FileIndex
0185     {
0186         a,
0187         b,
0188         c
0189     };
0190 
0191     std::map<QDateTime, FileIndex> dateMap;
0192 
0193     if(existsInA())
0194     {
0195         dateMap[getFileInfoA()->lastModified()] = FileIndex::a;
0196     }
0197     if(existsInB())
0198     {
0199         dateMap[getFileInfoB()->lastModified()] = FileIndex::b;
0200     }
0201     if(existsInC())
0202     {
0203         dateMap[getFileInfoC()->lastModified()] = FileIndex::c;
0204     }
0205 
0206     if(gOptions->m_bDmFullAnalysis)
0207     {
0208         if((existsInA() && isDirA()) || (existsInB() && isDirB()) || (existsInC() && isDirC()))
0209         {
0210             // If any input is a directory, don't start any comparison.
0211             m_bEqualAB = existsInA() && existsInB();
0212             m_bEqualAC = existsInA() && existsInC();
0213             m_bEqualBC = existsInB() && existsInC();
0214         }
0215         else
0216         {
0217             Q_EMIT pDMW->startDiffMerge(errors,
0218                 existsInA() ? getFileInfoA()->absoluteFilePath() : QString(""),
0219                 existsInB() ? getFileInfoB()->absoluteFilePath() : QString(""),
0220                 existsInC() ? getFileInfoC()->absoluteFilePath() : QString(""),
0221                 "",
0222                 "", "", "", &diffStatus());
0223             qint32 nofNonwhiteConflicts = diffStatus().getNonWhitespaceConflicts();
0224 
0225             if(gOptions->m_bDmWhiteSpaceEqual && nofNonwhiteConflicts == 0)
0226             {
0227                 m_bEqualAB = existsInA() && existsInB();
0228                 m_bEqualAC = existsInA() && existsInC();
0229                 m_bEqualBC = existsInB() && existsInC();
0230             }
0231             else
0232             {
0233                 m_bEqualAB = diffStatus().isBinaryEqualAB();
0234                 m_bEqualBC = diffStatus().isBinaryEqualBC();
0235                 m_bEqualAC = diffStatus().isBinaryEqualAC();
0236             }
0237 
0238             //Limit size of error list in memory.
0239             if(errors.size() >= 30)
0240             {
0241                 //Bail out something is very wrong.
0242                 return false;
0243             }
0244         }
0245     }
0246     else
0247     {
0248         bool bError = false;
0249         QString eqStatus;
0250         if(existsInA() && existsInB())
0251         {
0252             if(isDirA())
0253                 m_bEqualAB = true;
0254             else
0255                 m_bEqualAB = fastFileComparison(*getFileInfoA(), *getFileInfoB(), bError, eqStatus);
0256         }
0257         if(existsInA() && existsInC())
0258         {
0259             if(isDirA())
0260                 m_bEqualAC = true;
0261             else
0262                 m_bEqualAC = fastFileComparison(*getFileInfoA(), *getFileInfoC(), bError, eqStatus);
0263         }
0264         if(existsInB() && existsInC())
0265         {
0266             if((m_bEqualAB && m_bEqualAC) || isDirB())
0267                 m_bEqualBC = true;
0268             else
0269             {
0270                 m_bEqualBC = fastFileComparison(*getFileInfoB(), *getFileInfoC(), bError, eqStatus);
0271             }
0272         }
0273         if(bError)
0274         {
0275             //Limit size of error list in memory.
0276             if(errors.size() < 30)
0277                 errors.append(eqStatus);
0278             return false;
0279         }
0280     }
0281 
0282     if(isLinkA() != isLinkB()) m_bEqualAB = false;
0283     if(isLinkA() != isLinkC()) m_bEqualAC = false;
0284     if(isLinkB() != isLinkC()) m_bEqualBC = false;
0285 
0286     if(isDirA() != isDirB()) m_bEqualAB = false;
0287     if(isDirA() != isDirC()) m_bEqualAC = false;
0288     if(isDirB() != isDirC()) m_bEqualBC = false;
0289 
0290     assert(eNew == 0 && eMiddle == 1 && eOld == 2);
0291 
0292     // The map automatically sorts the keys.
0293     e_Age age = eNew;
0294     std::map<QDateTime, FileIndex>::reverse_iterator i;
0295     assert(dateMap.size() <= 3);
0296     for(i = dateMap.rbegin(); i != dateMap.rend(); ++i)
0297     {
0298         FileIndex n = i->second;
0299 
0300         if(n == FileIndex::a && getAgeA() == eNotThere)
0301         {
0302             setAgeA(age);
0303             age = nextAgeValue(age);
0304             if(m_bEqualAB)
0305             {
0306                 setAgeB(getAgeA());
0307                 age = nextAgeValue(age);
0308             }
0309             if(m_bEqualAC)
0310             {
0311                 setAgeC(getAgeA());
0312                 age = nextAgeValue(age);
0313             }
0314         }
0315         else if(n == FileIndex::b && getAgeB() == eNotThere)
0316         {
0317             setAgeB(age);
0318             age = nextAgeValue(age);
0319             if(m_bEqualAB)
0320             {
0321                 setAgeA(getAgeB());
0322                 age = nextAgeValue(age);
0323             }
0324             if(m_bEqualBC)
0325             {
0326                 setAgeC(getAgeB());
0327                 age = nextAgeValue(age);
0328             }
0329         }
0330         else if(n == FileIndex::c && getAgeC() == eNotThere)
0331         {
0332             setAgeC(age);
0333             age = nextAgeValue(age);
0334             if(m_bEqualAC)
0335             {
0336                 setAgeA(getAgeC());
0337                 age = nextAgeValue(age);
0338             }
0339             if(m_bEqualBC)
0340             {
0341                 setAgeB(getAgeC());
0342                 age = nextAgeValue(age);
0343             }
0344         }
0345     }
0346 
0347     // The checks below are necessary when the dates of the file are equal but the
0348     // files are not. One wouldn't expect this to happen, yet it happens sometimes.
0349     if(existsInC() && getAgeC() == eNotThere)
0350     {
0351         setAgeC(age);
0352         age = nextAgeValue(age);
0353         m_bConflictingAges = true;
0354     }
0355     if(existsInB() && getAgeB() == eNotThere)
0356     {
0357         setAgeB(age);
0358         age = nextAgeValue(age);
0359         m_bConflictingAges = true;
0360     }
0361     if(existsInA() && getAgeA() == eNotThere)
0362     {
0363         setAgeA(age);
0364         //unneeded as long as this is the last condition checked.
0365         //age = nextAgeValue(age);
0366         m_bConflictingAges = true;
0367     }
0368 
0369     if(getAgeA() != eOld && getAgeB() != eOld && getAgeC() != eOld)
0370     {
0371         if(getAgeA() == eMiddle) setAgeA(eOld);
0372         if(getAgeB() == eMiddle) setAgeB(eOld);
0373         if(getAgeC() == eMiddle) setAgeC(eOld);
0374     }
0375 
0376     return true;
0377 }
0378 
0379 bool MergeFileInfos::fastFileComparison(
0380     FileAccess& fi1, FileAccess& fi2,
0381     bool& bError, QString& status)
0382 {
0383     ProgressScope pp;
0384     bool bEqual = false;
0385 
0386     status = "";
0387     bError = true;
0388 
0389     qCDebug(kdiffMergeFileInfo) << "Entering MergeFileInfos::fastFileComparison";
0390     if(fi1.isNormal() != fi2.isNormal())
0391     {
0392         qCDebug(kdiffMergeFileInfo) << "Have: \'" << fi2.fileName() << "\' , isNormal = " << fi2.isNormal();
0393 
0394         status = i18n("Unable to compare non-normal file with normal file.");
0395         return false;
0396     }
0397 
0398     if(!fi1.isNormal())
0399     {
0400         qCInfo(kdiffMergeFileInfo) << "Skipping not a normal file.";
0401         bError = false;
0402         return false;
0403     }
0404 
0405     if(!gOptions->m_bDmFollowFileLinks)
0406     {
0407         qCInfo(kdiffMergeFileInfo) << "Have: \'" << fi2.fileName() << "\' , isSymLink = " << fi2.isSymLink();
0408         /*
0409             "git difftool --dir-diff"
0410             sets up directory comparsions with symlinks to the current work tree being compared with real files.
0411 
0412             m_bAllowMismatch is a compatibility work around.
0413         */
0414         if(fi1.isSymLink() != fi2.isSymLink())
0415         {
0416             qCDebug(kdiffMergeFileInfo) << "Rejecting comparison of link to file. With the same relitive path.";
0417             status = i18n("Mix of links and normal files.");
0418             return bEqual;
0419         }
0420         else if(fi1.isSymLink() && fi2.isSymLink())
0421         {
0422             qCDebug(kdiffMergeFileInfo) << "Comparison of link to link. OK.";
0423             bError = false;
0424             bEqual = fi1.readLink() == fi2.readLink();
0425             status = i18n("Link: ");
0426             return bEqual;
0427         }
0428     }
0429 
0430     if(fi1.size() != fi2.size())
0431     {
0432         qCInfo(kdiffMergeFileInfo) << "Sizes differ.";
0433         bError = false;
0434         bEqual = false;
0435         status = i18n("Size. ");
0436         return bEqual;
0437     }
0438     else if(gOptions->m_bDmTrustSize)
0439     {
0440         qCInfo(kdiffMergeFileInfo) << "Same size. Trusting result.";
0441 
0442         bEqual = true;
0443         bError = false;
0444         return bEqual;
0445     }
0446 
0447     if(gOptions->m_bDmTrustDate)
0448     {
0449         bEqual = (fi1.lastModified() == fi2.lastModified() && fi1.size() == fi2.size());
0450         bError = false;
0451         status = i18n("Date & Size: ");
0452         return bEqual;
0453     }
0454 
0455     if(gOptions->m_bDmTrustDateFallbackToBinary)
0456     {
0457         bEqual = (fi1.lastModified() == fi2.lastModified() && fi1.size() == fi2.size());
0458         if(bEqual)
0459         {
0460             bError = false;
0461             status = i18n("Date & Size: ");
0462             return bEqual;
0463         }
0464     }
0465 
0466     std::vector<char> buf1(100000);
0467     std::vector<char> buf2(buf1.size());
0468 
0469     if(!fi1.open(QIODevice::ReadOnly))
0470     {
0471         status = fi1.errorString();
0472         return bEqual;
0473     }
0474 
0475     if(!fi2.open(QIODevice::ReadOnly))
0476     {
0477         fi1.close();
0478         status = fi2.errorString();
0479         return bEqual;
0480     }
0481     qCInfo(kdiffMergeFileInfo) << "Comparing files...";
0482     ProgressProxy::setInformation(i18nc("Status message", "Comparing file..."), 0, false);
0483     typedef qint64 t_FileSize;
0484     t_FileSize fullSize = fi1.size();
0485     t_FileSize sizeLeft = fullSize;
0486 
0487     ProgressProxy::setMaxNofSteps(fullSize / buf1.size());
0488 
0489     while(sizeLeft > 0 && !ProgressProxy::wasCancelled())
0490     {
0491         qint64 len = std::min(sizeLeft, (t_FileSize)buf1.size());
0492         if(len != fi1.read(&buf1[0], len))
0493         {
0494             status = fi1.errorString();
0495             fi1.close();
0496             fi2.close();
0497             return bEqual;
0498         }
0499 
0500         if(len != fi2.read(&buf2[0], len))
0501         {
0502             status = fi2.errorString();
0503             fi1.close();
0504             fi2.close();
0505             return bEqual;
0506         }
0507 
0508         if(memcmp(&buf1[0], &buf2[0], len) != 0)
0509         {
0510             bError = false;
0511             fi1.close();
0512             fi2.close();
0513             return bEqual;
0514         }
0515         sizeLeft -= len;
0516         //ProgressProxy::setCurrent(double(fullSize-sizeLeft)/fullSize, false );
0517         ProgressProxy::step();
0518     }
0519     fi1.close();
0520     fi2.close();
0521 
0522     // If the program really arrives here, then the files are really equal.
0523     bError = false;
0524     bEqual = true;
0525     return bEqual;
0526 }
0527 
0528 void MergeFileInfos::updateAge()
0529 {
0530     if(isDirA() || isDirB() || isDirC())
0531     {
0532         setAgeA(eNotThere);
0533         setAgeB(eNotThere);
0534         setAgeC(eNotThere);
0535         e_Age age = eNew;
0536         if(existsInC())
0537         {
0538             setAgeC(age);
0539             if(m_bEqualAC) setAgeA(age);
0540             if(m_bEqualBC) setAgeB(age);
0541             age = eMiddle;
0542         }
0543         if(existsInB() && getAgeB() == eNotThere)
0544         {
0545             setAgeB(age);
0546             if(m_bEqualAB) setAgeA(age);
0547             age = eOld;
0548         }
0549         if(existsInA() && getAgeA() == eNotThere)
0550         {
0551             setAgeA(age);
0552         }
0553         if(getAgeA() != eOld && getAgeB() != eOld && getAgeC() != eOld)
0554         {
0555             if(getAgeA() == eMiddle) setAgeA(eOld);
0556             if(getAgeB() == eMiddle) setAgeB(eOld);
0557             if(getAgeC() == eMiddle) setAgeC(eOld);
0558         }
0559     }
0560 }
0561 
0562 QTextStream& operator<<(QTextStream& ts, MergeFileInfos& mfi)
0563 {
0564     ts << "{\n";
0565     ValueMap vm;
0566     vm.writeEntry("SubPath", mfi.subPath());
0567     vm.writeEntry("ExistsInA", mfi.existsInA());
0568     vm.writeEntry("ExistsInB", mfi.existsInB());
0569     vm.writeEntry("ExistsInC", mfi.existsInC());
0570     vm.writeEntry("EqualAB", mfi.isEqualAB());
0571     vm.writeEntry("EqualAC", mfi.isEqualAC());
0572     vm.writeEntry("EqualBC", mfi.isEqualBC());
0573 
0574     vm.writeEntry("MergeOperation", (qint32)mfi.getOperation());
0575     vm.writeEntry("DirA", mfi.isDirA());
0576     vm.writeEntry("DirB", mfi.isDirB());
0577     vm.writeEntry("DirC", mfi.isDirC());
0578     vm.writeEntry("LinkA", mfi.isLinkA());
0579     vm.writeEntry("LinkB", mfi.isLinkB());
0580     vm.writeEntry("LinkC", mfi.isLinkC());
0581     vm.writeEntry("OperationComplete", !mfi.isOperationRunning());
0582 
0583     vm.writeEntry("AgeA", (qint32)mfi.getAgeA());
0584     vm.writeEntry("AgeB", (qint32)mfi.getAgeB());
0585     vm.writeEntry("AgeC", (qint32)mfi.getAgeC());
0586     vm.writeEntry("ConflictingAges", mfi.conflictingAges()); // Equal age but files are not!
0587 
0588     vm.save(ts);
0589 
0590     ts << "}\n";
0591 
0592     return ts;
0593 }