#include "RepoFileIndex.h" #include #include #include #include #include #include #include #include #include namespace { // The cache of one root's last listing. The backend is part of the key so a // workspace that switches version control lists again instead of serving the // other backend's answer. struct CachedListing { RepoBackend backend = RepoBackend::None; RepoFileList list; qint64 stampMs = 0; }; // The one cache every call shares. The mutex covers the whole read-or-list-or- // store step, because the row provider ranks from a worker thread. struct RepoFileCache { QMutex mutex; QHash entries; static RepoFileCache& instance() { static RepoFileCache cache; return cache; } }; // The command as the error line spells it. [[nodiscard]] QString commandLine(const QString& program, const QStringList& arguments) { QStringList parts; parts.reserve(arguments.size() + 1); parts.append(program); parts.append(arguments); return parts.join(QLatin1Char(' ')); } // The last non-empty line of the command's standard error, or "". [[nodiscard]] QString lastErrorLine(const QByteArray& standardError) { const QList lines = standardError.split('\n'); for (const QByteArray& line : std::views::reverse(lines)) { const QByteArray trimmed = line.trimmed(); if (!trimmed.isEmpty()) { return QString::fromUtf8(trimmed); } } return {}; } // The paths of one NUL- or newline-separated listing. Empty entries drop, so // the empty tail after a final separator never becomes a path. [[nodiscard]] QStringList splitNonEmpty(const QByteArray& out, char separator) { QStringList paths; const QList entries = out.split(separator); paths.reserve(entries.size()); for (const QByteArray& entry : entries) { if (!entry.isEmpty()) { paths.append(QString::fromUtf8(entry)); } } return paths; } } // namespace RepoBackend repoBackendFromWord(const QString& word) { if (word == QLatin1String("jj")) { return RepoBackend::Jj; } if (word == QLatin1String("git")) { return RepoBackend::Git; } return RepoBackend::None; } RepoFileList RepoFileIndex::listFiles(const QString& root, RepoBackend backend) { RepoFileList result; if (backend == RepoBackend::None) { return result; } const bool git = backend == RepoBackend::Git; const QString program = git ? QStringLiteral("git") : QStringLiteral("jj"); // The working directory makes both commands print root-relative paths and // scopes them to root. core.quotepath=false keeps a non-ASCII path readable. const QStringList arguments = git ? QStringList{ QStringLiteral("-c"), QStringLiteral("core.quotepath=false"), QStringLiteral("ls-files"), QStringLiteral("-z"), QStringLiteral("--cached"), QStringLiteral("--others"), QStringLiteral("--exclude-standard") } : QStringList{ QStringLiteral("file"), QStringLiteral("list") }; QProcess process; process.setProgram(program); process.setArguments(arguments); process.setWorkingDirectory(root); process.setProcessChannelMode(QProcess::SeparateChannels); process.start(); const bool finished = process.waitForFinished(kTimeoutMs); if (!finished) { process.kill(); process.waitForFinished(1000); } const bool ok = finished && process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; if (!ok) { result.error = commandLine(program, arguments); const QString reason = lastErrorLine(process.readAllStandardError()); if (!reason.isEmpty()) { result.error += QStringLiteral(": ") + reason; } return result; } const QByteArray output = process.readAllStandardOutput(); result.paths = git ? parseGitLsFiles(output) : parseJjFileList(output); std::ranges::sort(result.paths); return result; } RepoFileList RepoFileIndex::cachedFiles(const QString& root, RepoBackend backend) { RepoFileCache& cache = RepoFileCache::instance(); const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); // The lock covers the whole read-or-list-or-store step, so two workers never // run the list command for one root at the same time. QMutexLocker locker(&cache.mutex); const auto it = cache.entries.constFind(root); if (it != cache.entries.constEnd() && it->backend == backend && nowMs - it->stampMs <= kTtlMs) { return it->list; } RepoFileList list = listFiles(root, backend); cache.entries.insert(root, CachedListing{ .backend = backend, .list = list, .stampMs = nowMs }); return list; } QStringList RepoFileIndex::parseJjFileList(const QByteArray& out) { return splitNonEmpty(out, '\n'); } QStringList RepoFileIndex::parseGitLsFiles(const QByteArray& out) { return splitNonEmpty(out, '\0'); } void RepoFileIndex::invalidate(const QString& root) { RepoFileCache& cache = RepoFileCache::instance(); QMutexLocker locker(&cache.mutex); if (root.isEmpty()) { cache.entries.clear(); } else { cache.entries.remove(root); } }