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 }