File indexing completed on 2024-05-12 05:51:44

0001 /*
0002     SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 #include "gitstatus.h"
0007 
0008 #include "hostprocess.h"
0009 #include <bytearraysplitter.h>
0010 #include <gitprocess.h>
0011 
0012 #include <KLocalizedString>
0013 #include <QByteArray>
0014 #include <QProcess>
0015 #include <QSet>
0016 
0017 #include <charconv>
0018 #include <optional>
0019 
0020 static void numStatForStatus(QList<GitUtils::StatusItem> &list, const QString &workDir, bool modified)
0021 {
0022     const auto args = modified ? QStringList{QStringLiteral("diff"), QStringLiteral("--numstat"), QStringLiteral("-z")}
0023                                : QStringList{QStringLiteral("diff"), QStringLiteral("--numstat"), QStringLiteral("--staged"), QStringLiteral("-z")};
0024 
0025     QProcess git;
0026     if (!setupGitProcess(git, workDir, args)) {
0027         return;
0028     }
0029     startHostProcess(git, QProcess::ReadOnly);
0030     if (git.waitForStarted() && git.waitForFinished(-1)) {
0031         if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
0032             return;
0033         }
0034     }
0035 
0036     GitUtils::parseDiffNumStat(list, git.readAllStandardOutput());
0037 }
0038 
0039 QByteArray fileNameFromPath(const QByteArray &path)
0040 {
0041     int lastSlash = path.lastIndexOf('/');
0042     return lastSlash == -1 ? path : path.mid(lastSlash + 1);
0043 }
0044 
0045 GitUtils::GitParsedStatus GitUtils::parseStatus(const QByteArray &raw, const QString &workingDir)
0046 {
0047     QList<GitUtils::StatusItem> untracked;
0048     QList<GitUtils::StatusItem> unmerge;
0049     QList<GitUtils::StatusItem> staged;
0050     QList<GitUtils::StatusItem> changed;
0051 
0052     for (auto r : ByteArraySplitter(raw, '\0')) {
0053         if (r.length() < 3) {
0054             continue;
0055         }
0056 
0057         char x = r.at(0);
0058         char y = r.at(1);
0059         uint16_t xy = (((uint16_t)x) << 8) | y;
0060         using namespace GitUtils;
0061 
0062         r.remove_prefix(3);
0063         QByteArray file = r.toByteArray();
0064 
0065         switch (xy) {
0066         case StatusXY::QQ:
0067             untracked.append({file, GitStatus::Untracked, 'U', 0, 0});
0068             break;
0069         case StatusXY::II:
0070             untracked.append({file, GitStatus::Ignored, 'I', 0, 0});
0071             break;
0072 
0073         case StatusXY::DD:
0074             unmerge.append({file, GitStatus::Unmerge_BothDeleted, x, 0, 0});
0075             break;
0076         case StatusXY::AU:
0077             unmerge.append({file, GitStatus::Unmerge_AddedByUs, x, 0, 0});
0078             break;
0079         case StatusXY::UD:
0080             unmerge.append({file, GitStatus::Unmerge_DeletedByThem, x, 0, 0});
0081             break;
0082         case StatusXY::UA:
0083             unmerge.append({file, GitStatus::Unmerge_AddedByThem, x, 0, 0});
0084             break;
0085         case StatusXY::DU:
0086             unmerge.append({file, GitStatus::Unmerge_DeletedByUs, x, 0, 0});
0087             break;
0088         case StatusXY::AA:
0089             unmerge.append({file, GitStatus::Unmerge_BothAdded, x, 0, 0});
0090             break;
0091         case StatusXY::UU:
0092             unmerge.append({file, GitStatus::Unmerge_BothModified, x, 0, 0});
0093             break;
0094         }
0095 
0096         switch (x) {
0097         case 'M':
0098             staged.append({file, GitStatus::Index_Modified, x, 0, 0});
0099             break;
0100         case 'A':
0101             staged.append({file, GitStatus::Index_Added, x, 0, 0});
0102             break;
0103         case 'D':
0104             staged.append({file, GitStatus::Index_Deleted, x, 0, 0});
0105             break;
0106         case 'R':
0107             staged.append({file, GitStatus::Index_Renamed, x, 0, 0});
0108             break;
0109         case 'C':
0110             staged.append({file, GitStatus::Index_Copied, x, 0, 0});
0111             break;
0112         }
0113 
0114         switch (y) {
0115         case 'M':
0116             changed.append({file, GitStatus::WorkingTree_Modified, y, 0, 0});
0117             break;
0118         case 'D':
0119             changed.append({file, GitStatus::WorkingTree_Deleted, y, 0, 0});
0120             break;
0121         case 'A':
0122             changed.append({file, GitStatus::WorkingTree_IntentToAdd, y, 0, 0});
0123             break;
0124         }
0125     }
0126 
0127     QSet<QString> nonUniqueFileNames;
0128     QSet<QByteArray> seen;
0129     auto getNonUniqueFileNamesFor = [&nonUniqueFileNames, &seen](const QList<GitUtils::StatusItem> &items) {
0130         for (const auto &c : items) {
0131             const auto file = fileNameFromPath(c.file);
0132             if (seen.contains(file)) {
0133                 nonUniqueFileNames.insert(QString::fromUtf8(file));
0134             } else {
0135                 seen.insert(file);
0136             }
0137         }
0138     };
0139     getNonUniqueFileNamesFor(changed);
0140     getNonUniqueFileNamesFor(staged);
0141     getNonUniqueFileNamesFor(unmerge);
0142     // Nothing for untracked as untracked items can be in thousands
0143 
0144     numStatForStatus(changed, workingDir, true);
0145     numStatForStatus(staged, workingDir, false);
0146 
0147     return {untracked, unmerge, staged, changed, nonUniqueFileNames};
0148 }
0149 
0150 QString GitUtils::statusString(GitUtils::GitStatus s)
0151 {
0152     switch (s) {
0153     case WorkingTree_Modified:
0154     case Index_Modified:
0155         return i18n(" ‣ Modified");
0156     case Untracked:
0157         return i18n(" ‣ Untracked");
0158     case Index_Renamed:
0159         return i18n(" ‣ Renamed");
0160     case Index_Deleted:
0161     case WorkingTree_Deleted:
0162         return i18n(" ‣ Deleted");
0163     case Index_Added:
0164     case WorkingTree_IntentToAdd:
0165         return i18n(" ‣ Added");
0166     case Index_Copied:
0167         return i18n(" ‣ Copied");
0168     case Ignored:
0169         return i18n(" ‣ Ignored");
0170     case Unmerge_AddedByThem:
0171     case Unmerge_AddedByUs:
0172     case Unmerge_BothAdded:
0173     case Unmerge_BothDeleted:
0174     case Unmerge_BothModified:
0175     case Unmerge_DeletedByThem:
0176     case Unmerge_DeletedByUs:
0177         return i18n(" ‣ Conflicted");
0178     }
0179     return QString();
0180 }
0181 
0182 static void addNumStat(QList<GitUtils::StatusItem> &items, int add, int sub, std::string_view file)
0183 {
0184     // look in modified first, then staged
0185     auto item = std::find_if(items.begin(), items.end(), [file](const GitUtils::StatusItem &si) {
0186         return file.compare(0, si.file.size(), si.file.data()) == 0;
0187     });
0188     if (item != items.end()) {
0189         item->linesAdded = add;
0190         item->linesRemoved = sub;
0191         return;
0192     }
0193 }
0194 
0195 static std::optional<int> toInt(std::string_view s)
0196 {
0197     int value{};
0198     auto res = std::from_chars(s.data(), s.data() + s.size(), value);
0199     if (res.ptr == (s.data() + s.size())) {
0200         return value;
0201     }
0202     return std::nullopt;
0203 }
0204 
0205 void GitUtils::parseDiffNumStat(QList<GitUtils::StatusItem> &items, const QByteArray &raw)
0206 {
0207     // format:
0208     // 12\t10\tFileName
0209     // 12 = add, 10 = sub, fileName at the end
0210     for (auto line : ByteArraySplitter(raw, '\0')) {
0211         size_t addEnd = line.find_first_of('\t');
0212         if (addEnd == std::string_view::npos) {
0213             continue;
0214         }
0215 
0216         size_t subStart = line.find_first_not_of('\t', addEnd);
0217         if (subStart == std::string_view::npos) {
0218             continue;
0219         }
0220 
0221         size_t subEnd = line.find_first_of('\t', subStart);
0222         if (subEnd == std::string_view::npos) {
0223             continue;
0224         }
0225 
0226         std::string_view addStr = line.substr(0, addEnd);
0227         std::string_view subStr = line.substr(subStart, subEnd - subStart);
0228         std::string_view fileStr = line.substr(subEnd + 1, line.size() - (subEnd + 1));
0229 
0230         auto add = toInt(addStr);
0231         auto sub = toInt(subStr);
0232 
0233         if (!add.has_value()) {
0234             continue;
0235         }
0236         if (!sub.has_value()) {
0237             continue;
0238         }
0239 
0240         addNumStat(items, add.value(), sub.value(), fileStr);
0241     }
0242 }
0243 
0244 QList<GitUtils::StatusItem> GitUtils::parseDiffNameStatus(const QByteArray &raw)
0245 {
0246     QList<GitUtils::StatusItem> out;
0247     for (auto l : ByteArraySplitter(raw, '\n')) {
0248         ByteArraySplitter splitter(l, '\t');
0249         if (splitter.empty()) {
0250             continue;
0251         }
0252         auto it = splitter.begin();
0253         GitUtils::StatusItem i;
0254         i.statusChar = (*it).at(0);
0255 
0256         ++it;
0257         if (it == splitter.end()) {
0258             continue;
0259         }
0260         i.file = (*it).toByteArray();
0261         out.append(i);
0262     }
0263     return out;
0264 }