#include "GitBridge.h" #include #include GitBridge::GitBridge(QObject* parent) : QObject(parent) {} QProcess* GitBridge::run(const QStringList& args, const QString& workingDirectory) { auto* process = new QProcess(); if (!workingDirectory.isEmpty()) { process->setWorkingDirectory(workingDirectory); } process->setProcessChannelMode(QProcess::SeparateChannels); process->setProgram(QStringLiteral("git")); process->setArguments(args); // The process owns itself: after emitting finished it schedules a deferred // delete, which runs after every finished handler (including callers that // read stdout/stderr) has completed. A shared_ptr here would double-free: // deleteLater() deletes the raw object, then the shared_ptr refcount held // by the connection functor reaches zero and deletes it again. QObject::connect(process, &QProcess::finished, this, [this, process]() { m_busy = false; emit busyChanged(); process->deleteLater(); }); m_busy = true; emit busyChanged(); process->start(); return process; } void GitBridge::repoRoot(const QString& path) { QString current = QDir::cleanPath(path); if (current.isEmpty()) { current = QDir::currentPath(); } QString foundRoot; while (true) { const QString canonical = QFileInfo(current).canonicalFilePath(); if (canonical.isEmpty()) { break; } // Worktrees use a `.git` file pointing at the main repo's gitdir, while a // normal checkout has a `.git` directory; accept either. if (QFileInfo(canonical + QStringLiteral("/.git")).isDir() || QFileInfo(canonical + QStringLiteral("/.git")).isFile()) { foundRoot = canonical; break; } const QString parent = QFileInfo(canonical).absolutePath(); if (parent == canonical) { break; } current = parent; } emit repoRootResolved(path, foundRoot); } void GitBridge::listWorktrees(const QString& repoRoot) { const QStringList args = {QStringLiteral("-C"), repoRoot, QStringLiteral("worktree"), QStringLiteral("list"), QStringLiteral("--porcelain")}; QProcess* process = run(args, QString()); QObject::connect(process, &QProcess::finished, this, [this, process, repoRoot]() { const QByteArray output = process->readAllStandardOutput(); QVariantList items; const QStringList blocks = QString::fromUtf8(output).split(QStringLiteral("\n\n"), Qt::SkipEmptyParts); for (const QString& block : blocks) { QString path; QString name; for (const QString& line : block.split(QLatin1Char('\n'), Qt::SkipEmptyParts)) { if (line.startsWith(QStringLiteral("worktree "))) { path = line.mid(static_cast(QStringLiteral("worktree ").size())); } else if (line.startsWith(QStringLiteral("branch "))) { const QString branch = line.mid(static_cast(QStringLiteral("branch ").size())); if (branch.startsWith(QStringLiteral("refs/heads/"))) { name = branch.mid(static_cast( QStringLiteral("refs/heads/").size())); } } else if (line == QStringLiteral("detached")) { name = QFileInfo(path).fileName(); } } if (path.isEmpty()) { continue; } // The current checkout is not a pickable worktree. if (QFileInfo(path).canonicalFilePath() == QFileInfo(repoRoot).canonicalFilePath()) { continue; } if (name.isEmpty()) { name = QFileInfo(path).fileName(); } QVariantMap item; item.insert(QStringLiteral("name"), name); item.insert(QStringLiteral("path"), path); items.append(item); } emit worktreesListed(items); }); } void GitBridge::addWorktree(const QString& repoRoot, const QString& path, const QString& name) { const QStringList args = {QStringLiteral("-C"), repoRoot, QStringLiteral("worktree"), QStringLiteral("add"), QStringLiteral("-b"), name, path}; QProcess* process = run(args, QString()); QObject::connect(process, &QProcess::finished, this, [this, process, path, name]() { const bool ok = process->exitStatus() == QProcess::NormalExit && process->exitCode() == 0; if (ok) { emit worktreeAdded(true, QString(), QFileInfo(path).canonicalFilePath(), name); } else { emit worktreeAdded( false, QString::fromUtf8(process->readAllStandardError()).trimmed(), QString(), name); } }); } void GitBridge::identify(const QString& path) { QString current = QDir::cleanPath(path); if (current.isEmpty()) { current = QDir::currentPath(); } QProcess* process = run({QStringLiteral("-C"), current, QStringLiteral("rev-parse"), QStringLiteral("--show-toplevel")}, QString()); QObject::connect(process, &QProcess::finished, this, [this, process, path, current]() { const bool ok = process->exitStatus() == QProcess::NormalExit && process->exitCode() == 0; if (!ok) { emit identified(path, QString(), false); return; } const QString ownTop = QDir::cleanPath( QString::fromUtf8(process->readAllStandardOutput()) .trimmed()); // The common dir's toplevel is the main repository root. QProcess* common = run({QStringLiteral("-C"), current, QStringLiteral("rev-parse"), QStringLiteral("--git-common-dir")}, QString()); QObject::connect(common, &QProcess::finished, this, [this, common, path, current, ownTop]() { const bool ok = common->exitStatus() == QProcess::NormalExit && common->exitCode() == 0; if (!ok) { emit identified(path, QString(), false); return; } QString commonDir = QString::fromUtf8( common->readAllStandardOutput()) .trimmed(); if (QFileInfo(commonDir).isRelative()) { // git prints a relative path when the // command runs inside the repository. commonDir = QDir::cleanPath( QFileInfo(current, commonDir) .absoluteFilePath()); } const QFileInfo commonInfo(commonDir); QString repoRoot; if (commonInfo.isDir()) { // The common dir is the .git directory; // its parent is the main repository root. repoRoot = QDir::cleanPath(commonInfo.absolutePath()); } const bool isWorktree = !repoRoot.isEmpty() && QFileInfo(ownTop).canonicalFilePath() != QFileInfo(repoRoot).canonicalFilePath(); emit identified(path, repoRoot, isWorktree); }); }); }