Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
5.2 kB · 176 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177#include "RepoFileIndex.h"
#include <QByteArray>#include <QDateTime>#include <QHash>#include <QMutex>#include <QMutexLocker>#include <QProcess>#include <QStringList>
#include <algorithm>#include <ranges>
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<QString, CachedListing> 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<QByteArray> 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<QByteArray> 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); }}