diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ccade5cb..4073a6362 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ name: Gittyup on: push: branches: - - master + - latest tags: - 'gittyup_v*' pull_request: @@ -381,12 +381,12 @@ jobs: # https://github.com/marvinpinto/actions/issues/177 needs: [flatpak, build] runs-on: ubuntu-latest # does not matter which - # a prerelase is created when pushing to master + # a prerelase is created when pushing to latest # a release is created when a tag will be set # last condition is the same as IS_RELEASE, # but environment variables cannot be used outside of steps # so it was copied to here too - if: ${{ github.ref == 'refs/heads/master' || (github.event_name == 'push' && github.ref_type == 'tag' && startswith(github.ref_name, 'gittyup_v')) }} + if: ${{ github.ref == 'refs/heads/latest' || (github.event_name == 'push' && github.ref_type == 'tag' && startswith(github.ref_name, 'gittyup_v')) }} steps: - name: Download artifacts uses: actions/download-artifact@v3 diff --git a/.gitignore b/.gitignore index 6786e6c10..1ed155661 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ build +.cache .DS_Store .project .vscode/ CMakeLists.txt.user cmake-build-debug/ cmake-build-release/ -build .idea/ -.venv +.venv/ +compile_commands.json + diff --git a/src/tools/DiffTool.cpp b/src/tools/DiffTool.cpp index 67019a67d..6f047faa7 100644 --- a/src/tools/DiffTool.cpp +++ b/src/tools/DiffTool.cpp @@ -10,17 +10,23 @@ #include "DiffTool.h" #include "git/Command.h" #include "git/Repository.h" +#include "git/Blob.h" #include #include #include -DiffTool::DiffTool(const QString &file, const git::Blob &localBlob, - const git::Blob &remoteBlob, QObject *parent) - : ExternalTool(file, parent), mLocalBlob(localBlob), - mRemoteBlob(remoteBlob) {} +DiffTool::DiffTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent) + : ExternalTool(files, diff, repo, parent) {} bool DiffTool::isValid() const { - return (ExternalTool::isValid() && mLocalBlob.isValid()); + bool isValid = ExternalTool::isValid(); + foreach (const QString &file, mFiles) { + git::Blob fileBlob; + isValid &= DiffTool::getBlob(file, git::Diff::OldFile, fileBlob) & + fileBlob.isValid(); + }; + return isValid; } ExternalTool::Kind DiffTool::kind() const { return Diff; } @@ -35,72 +41,75 @@ bool DiffTool::start() { if (command.isEmpty()) return false; - // Write temporary files. - QString templatePath = QDir::temp().filePath(QFileInfo(mFile).fileName()); - QTemporaryFile *local = new QTemporaryFile(templatePath, this); - if (!local->open()) - return false; - - local->write(mLocalBlob.content()); - local->flush(); - - QString remotePath; - if (!mRemoteBlob.isValid()) { - remotePath = mFile; - } else { - QTemporaryFile *remote = new QTemporaryFile(templatePath, this); - if (!remote->open()) - return false; - - remote->write(mRemoteBlob.content()); - remote->flush(); - - remotePath = remote->fileName(); - } - - // Destroy this after process finishes. - QProcess *process = new QProcess(this); - process->setProcessChannelMode( - QProcess::ProcessChannelMode::ForwardedChannels); - auto signal = QOverload::of(&QProcess::finished); - QObject::connect(process, signal, [this, process] { - qDebug() << "Merge Process Exited!"; - qDebug() << "Stdout: " << process->readAllStandardOutput(); - qDebug() << "Stderr: " << process->readAllStandardError(); - deleteLater(); - }); + bool isWorkDirDiff = mDiff.isValid() && mDiff.isStatusDiff(); + + int numFiles = mFiles.size(); + foreach (const QString &filePathAndName, mFiles) { + git::Blob filePathOld, filePathNew; + if (!getBlob(filePathAndName, git::Diff::OldFile, filePathOld) || + !getBlob(filePathAndName, git::Diff::NewFile, filePathNew)) + continue; + + // Get the path to the file (either a full or relative path). + QString otherPathAndName = filePathAndName; + if (filePathOld.isValid()) { + otherPathAndName = makeBlobTempFullFilePath(filePathAndName, filePathOld); + if (otherPathAndName.isEmpty()) + return false; + } + + // Destroy this after process finishes. + QProcess *process = new QProcess(); + process->setProcessChannelMode( + QProcess::ProcessChannelMode::ForwardedChannels); + auto signal = QOverload::of(&QProcess::finished); + QObject::connect(process, signal, [this, process, &numFiles] { + qDebug() << "Merge Process Exited!"; + qDebug() << "Stdout: " << process->readAllStandardOutput(); + qDebug() << "Stderr: " << process->readAllStandardError(); + if (--numFiles == 0) { + deleteLater(); + } + }); + + // Convert to absolute path. + QString fullFilePath = + isWorkDirDiff ? mRepo.workdir().filePath(filePathAndName) + : makeBlobTempFullFilePath(filePathAndName, filePathNew); #if defined(FLATPAK) || defined(DEBUG_FLATPAK) - QStringList arguments = {"--host", "--env=LOCAL=" + local->fileName(), - "--env=REMOTE=" + remotePath, - "--env=MERGED=" + mFile, "--env=BASE=" + mFile}; - arguments.append("sh"); - arguments.append("-c"); - arguments.append(command); - process->start("flatpak-spawn", arguments); + QStringList arguments = {"--host", "--env=LOCAL=" + fullFilePath, + "--env=REMOTE=" + otherPathAndName, + "--env=MERGED=" + filePathAndName, + "--env=BASE=" + filePathAndName}; + arguments.append("sh"); + arguments.append("-c"); + arguments.append(command); + process->start("flatpak-spawn", arguments); #else - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.insert("LOCAL", local->fileName()); - env.insert("REMOTE", remotePath); - env.insert("MERGED", mFile); - env.insert("BASE", mFile); - process->setProcessEnvironment(env); - - QString bash = git::Command::bashPath(); - if (!bash.isEmpty()) { - process->start(bash, {"-c", command}); - } else if (!shell) { - process->start(git::Command::substitute(env, command)); - } else { - emit error(BashNotFound); - return false; - } + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("LOCAL", fullFilePath); + env.insert("REMOTE", otherPathAndName); + env.insert("MERGED", filePathAndName); + env.insert("BASE", filePathAndName); + process->setProcessEnvironment(env); + + QString bash = git::Command::bashPath(); + if (!bash.isEmpty()) { + process->start(bash, {"-c", command}); + } else if (!shell) { + process->start(git::Command::substitute(env, command)); + } else { + emit error(BashNotFound); + return false; + } #endif - if (!process->waitForStarted()) { - qDebug() << "DiffTool starting failed"; - return false; + if (!process->waitForStarted()) { + qDebug() << "DiffTool starting failed"; + return false; + } } // Detach from parent. @@ -108,3 +117,29 @@ bool DiffTool::start() { return true; } + +bool DiffTool::getBlob(const QString &file, const git::Diff::File &version, + git::Blob &blob) const { + int index = mDiff.indexOf(file); + if (index < 0) + return false; + + blob = mRepo.lookupBlob(mDiff.id(index, version)); + return true; +} + +QString DiffTool::makeBlobTempFullFilePath(const QString &filePathAndName, + const git::Blob &fileBlob) { + QString blobTmpFullFilePath; + + QFileInfo fileInfo(filePathAndName); + QString templatePath = QDir::temp().filePath(fileInfo.fileName()); + QTemporaryFile *temp = new QTemporaryFile(templatePath, this); + if (temp->open()) { + temp->write(fileBlob.content()); + temp->flush(); + blobTmpFullFilePath = temp->fileName(); + } + + return blobTmpFullFilePath; +} diff --git a/src/tools/DiffTool.h b/src/tools/DiffTool.h index 29246164f..8b541603e 100644 --- a/src/tools/DiffTool.h +++ b/src/tools/DiffTool.h @@ -10,15 +10,23 @@ #ifndef DIFFTOOL_H #define DIFFTOOL_H +#include #include "ExternalTool.h" -#include "git/Blob.h" + +class QObject; +class QString; +namespace git { +class Diff; +class Repository; +class Blob; +}; // namespace git class DiffTool : public ExternalTool { Q_OBJECT public: - DiffTool(const QString &file, const git::Blob &localBlob, - const git::Blob &remoteBlob, QObject *parent = nullptr); + DiffTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent); bool isValid() const override; @@ -28,8 +36,12 @@ class DiffTool : public ExternalTool { bool start() override; protected: - git::Blob mLocalBlob; - git::Blob mRemoteBlob; +private: + bool getBlob(const QString &file, const git::Diff::File &version, + git::Blob &blob) const; + + QString makeBlobTempFullFilePath(const QString &filePathAndName, + const git::Blob &fileBlob); }; #endif diff --git a/src/tools/EditTool.cpp b/src/tools/EditTool.cpp index f27ed0278..21c2ee981 100644 --- a/src/tools/EditTool.cpp +++ b/src/tools/EditTool.cpp @@ -14,11 +14,19 @@ #include #include -EditTool::EditTool(const QString &file, QObject *parent) - : ExternalTool(file, parent) {} +EditTool::EditTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent) + : ExternalTool(files, diff, repo, parent) {} bool EditTool::isValid() const { - return (ExternalTool::isValid() && QFileInfo(mFile).isFile()); + if (!ExternalTool::isValid()) + return false; + + foreach (const QString file, mFiles) { + if (!QFileInfo(mRepo.workdir().filePath(file)).isFile()) + return false; + } + return true; } ExternalTool::Kind EditTool::kind() const { return Edit; } @@ -27,26 +35,31 @@ QString EditTool::name() const { return tr("Edit in External Editor"); } bool EditTool::start() { git::Config config = git::Config::global(); - QString editor = config.value("gui.editor"); + QString baseEditor = config.value("gui.editor"); - if (editor.isEmpty()) - editor = qgetenv("GIT_EDITOR"); + if (baseEditor.isEmpty()) + baseEditor = qgetenv("GIT_EDITOR"); - if (editor.isEmpty()) - editor = config.value("core.editor"); + if (baseEditor.isEmpty()) + baseEditor = config.value("core.editor"); - if (editor.isEmpty()) - editor = qgetenv("VISUAL"); + if (baseEditor.isEmpty()) + baseEditor = qgetenv("VISUAL"); - if (editor.isEmpty()) - editor = qgetenv("EDITOR"); + if (baseEditor.isEmpty()) + baseEditor = qgetenv("EDITOR"); - if (editor.isEmpty()) - return QDesktopServices::openUrl(QUrl::fromLocalFile(mFile)); + if (baseEditor.isEmpty()) { + foreach (const QString &file, mFiles) { + QDesktopServices::openUrl(QUrl::fromLocalFile(file)); + } + return true; + } - // Find arguments. - QStringList args = editor.split("\" \""); + QString editor = baseEditor; + // Find arguments. + QStringList args = baseEditor.split("\" \""); if (args.count() > 1) { // Format 1: "Command" "Argument1" "Argument2" editor = args[0]; @@ -62,17 +75,24 @@ bool EditTool::start() { editor = editor.left(li + 1); } else { // Format 3: "Command" (no argument) - // Format 4: Command (no argument) + if (fi == -1) { + // Format 4: Command Argument1 Argument2 + // Format 5: Command (no argument) + args = editor.split(" "); + editor = args.size() ? args[0] : ""; + } } } // Remove command, add filename, trim command. args.removeFirst(); - args.append(mFile); + foreach (const QString &file, mFiles) { + args.append(mRepo.workdir().filePath(file)); + } editor.remove("\""); // Destroy this after process finishes. - QProcess *process = new QProcess(this); + QProcess *process = new QProcess(); auto signal = QOverload::of(&QProcess::finished); QObject::connect(process, signal, this, &ExternalTool::deleteLater); diff --git a/src/tools/EditTool.h b/src/tools/EditTool.h index 43bf542a9..1ee62ae33 100644 --- a/src/tools/EditTool.h +++ b/src/tools/EditTool.h @@ -12,11 +12,18 @@ #include "ExternalTool.h" +class QObject; +namespace git { +class Diff; +class Repository; +}; // namespace git + class EditTool : public ExternalTool { Q_OBJECT public: - EditTool(const QString &file, QObject *parent = nullptr); + EditTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent); bool isValid() const override; diff --git a/src/tools/ExternalTool.cpp b/src/tools/ExternalTool.cpp index c27f4d2c1..4748491b0 100644 --- a/src/tools/ExternalTool.cpp +++ b/src/tools/ExternalTool.cpp @@ -13,7 +13,7 @@ #include "conf/Settings.h" #include "git/Diff.h" #include "git/Config.h" -#include "git/Index.h" +#include "git/Blob.h" #include "git/Repository.h" #include #include @@ -39,10 +39,13 @@ void splitCommand(const QString &command, QString &program, QString &args) { } // namespace -ExternalTool::ExternalTool(const QString &file, QObject *parent) - : QObject(parent), mFile(file) {} +ExternalTool::ExternalTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent) + : QObject(parent), mFiles(files), mDiff(diff), mRepo(repo) {} -bool ExternalTool::isValid() const { return !mFile.isEmpty(); } +bool ExternalTool::isValid() const { + return !mFiles.isEmpty() && !mFiles.first().isEmpty(); +} QString ExternalTool::lookupCommand(const QString &key, bool &shell) { git::Config config = git::Config::global(); @@ -96,30 +99,14 @@ QList ExternalTool::readBuiltInTools(const QString &key) { return tools; } -ExternalTool *ExternalTool::create(const QString &file, const git::Diff &diff, - const git::Repository &repo, - QObject *parent) { - if (!diff.isValid()) - return nullptr; +bool ExternalTool::isConflicted(const QString &file) const { + if (!mDiff.isValid()) + return false; - int index = diff.indexOf(file); + int index = mDiff.indexOf(file); if (index < 0) - return nullptr; - - // Convert to absolute path. - QString path = repo.workdir().filePath(file); + return false; // Create merge tool. - if (diff.status(index) == GIT_DELTA_CONFLICTED) { - git::Index::Conflict conflict = repo.index().conflict(file); - git::Blob local = repo.lookupBlob(conflict.ours); - git::Blob remote = repo.lookupBlob(conflict.theirs); - git::Blob base = repo.lookupBlob(conflict.ancestor); - return new MergeTool(path, local, remote, base, parent); - } - - // Create diff tool. - git::Blob local = repo.lookupBlob(diff.id(index, git::Diff::OldFile)); - git::Blob remote = repo.lookupBlob(diff.id(index, git::Diff::NewFile)); - return new DiffTool(path, local, remote, parent); + return mDiff.status(index) == GIT_DELTA_CONFLICTED; } diff --git a/src/tools/ExternalTool.h b/src/tools/ExternalTool.h index 87a9a88fe..3ce6e577d 100644 --- a/src/tools/ExternalTool.h +++ b/src/tools/ExternalTool.h @@ -12,11 +12,8 @@ #include #include - -namespace git { -class Diff; -class Repository; -} // namespace git +#include "git/Diff.h" +#include "git/Repository.h" class ExternalTool : public QObject { Q_OBJECT @@ -40,7 +37,8 @@ class ExternalTool : public QObject { } }; - ExternalTool(const QString &file, QObject *parent = nullptr); + ExternalTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent); virtual bool isValid() const; @@ -49,19 +47,19 @@ class ExternalTool : public QObject { virtual bool start() = 0; + bool isConflicted(const QString &file) const; + static QString lookupCommand(const QString &key, bool &shell); static QList readGlobalTools(const QString &key); static QList readBuiltInTools(const QString &key); - static ExternalTool *create(const QString &file, const git::Diff &diff, - const git::Repository &repo, - QObject *parent = nullptr); - signals: void error(Error error); protected: - QString mFile; + QStringList mFiles; + git::Diff mDiff; + git::Repository mRepo; }; #endif diff --git a/src/tools/MergeTool.cpp b/src/tools/MergeTool.cpp index 677f679b2..e39ac7523 100644 --- a/src/tools/MergeTool.cpp +++ b/src/tools/MergeTool.cpp @@ -19,15 +19,32 @@ #include #include -MergeTool::MergeTool(const QString &file, const git::Blob &localBlob, - const git::Blob &remoteBlob, const git::Blob &baseBlob, - QObject *parent) - : ExternalTool(file, parent), mLocalBlob(localBlob), - mRemoteBlob(remoteBlob), mBaseBlob(baseBlob) {} +MergeTool::MergeTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent) + : ExternalTool(files, diff, repo, parent) { + + if (!mFiles.empty()) { + foreach (const QString &file, mFiles) { + if (isConflicted(file)) { + git::Index::Conflict conflict = repo.index().conflict(file); + mMerges.append({file, repo.lookupBlob(conflict.ours), + repo.lookupBlob(conflict.theirs), + repo.lookupBlob(conflict.ancestor)}); + } + } + } +} bool MergeTool::isValid() const { - return (ExternalTool::isValid() && mLocalBlob.isValid() && - mRemoteBlob.isValid()); + if (!ExternalTool::isValid()) + return false; + + for (const FileMerge &fileMerge : mMerges) { + if (!fileMerge.local.isValid() || !fileMerge.remote.isValid()) { + return false; + } + } + return true; } ExternalTool::Kind MergeTool::kind() const { return Merge; } @@ -42,101 +59,111 @@ bool MergeTool::start() { if (command.isEmpty()) return false; - // Write temporary files. - QString templatePath = QDir::temp().filePath(QFileInfo(mFile).fileName()); - QTemporaryFile *local = new QTemporaryFile(templatePath, this); - if (!local->open()) - return false; - - local->write(mLocalBlob.content()); - local->flush(); - - QTemporaryFile *remote = new QTemporaryFile(templatePath, this); - if (!remote->open()) - return false; + int numMergeFiles = mMerges.size(); + for (const FileMerge &fileMerge : mMerges) { + // Write temporary files. + QString templatePath = + QDir::temp().filePath(QFileInfo(fileMerge.name).fileName()); + QTemporaryFile *local = new QTemporaryFile(templatePath, this); + if (!local->open()) + return false; - remote->write(mRemoteBlob.content()); - remote->flush(); + local->write(fileMerge.local.content()); + local->flush(); - QString basePath; - if (mBaseBlob.isValid()) { - QTemporaryFile *base = new QTemporaryFile(templatePath, this); - if (!base->open()) + QTemporaryFile *remote = new QTemporaryFile(templatePath, this); + if (!remote->open()) return false; - base->write(mBaseBlob.content()); - base->flush(); + remote->write(fileMerge.remote.content()); + remote->flush(); - basePath = base->fileName(); - } + QString basePath; + if (fileMerge.base.isValid()) { + QTemporaryFile *base = new QTemporaryFile(templatePath, this); + if (!base->open()) + return false; - // Make the backup copy. - QString backupPath = QString("%1.orig").arg(mFile); - if (!QFile::copy(mFile, backupPath)) { - // FIXME: What should happen if the backup already exists? - } + base->write(fileMerge.base.content()); + base->flush(); - // Destroy this after process finishes. - QProcess *process = new QProcess(this); - process->setProcessChannelMode( - QProcess::ProcessChannelMode::ForwardedChannels); - git::Repository repo = mLocalBlob.repo(); - auto signal = QOverload::of(&QProcess::finished); - QObject::connect(process, signal, [this, repo, backupPath, process] { - Debug("Merge Process Exited!"); - Debug("Stdout: " << process->readAllStandardOutput()); - Debug("Stderr: " << process->readAllStandardError()); - - QFileInfo merged(mFile); - QFileInfo backup(backupPath); - git::Config config = git::Config::global(); - bool modified = (merged.lastModified() > backup.lastModified()); - if (!modified || !config.value("mergetool.keepBackup")) - QFile::remove(backupPath); - - if (modified) { - int length = repo.workdir().path().length(); - repo.index().setStaged({mFile.mid(length + 1)}, true); + basePath = base->fileName(); } - deleteLater(); - }); + // Make the backup copy. + QString backupPath = QString("%1.orig").arg(fileMerge.name); + if (!QFile::copy(fileMerge.name, backupPath)) { + // FIXME: What should happen if the backup already exists? + } - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.insert("LOCAL", local->fileName()); - env.insert("REMOTE", remote->fileName()); - env.insert("MERGED", mFile); - env.insert("BASE", basePath); - process->setProcessEnvironment(env); + // Destroy this after process finishes. + QProcess *process = new QProcess(); + process->setProcessChannelMode( + QProcess::ProcessChannelMode::ForwardedChannels); + git::Repository repo = fileMerge.local.repo(); + auto signal = QOverload::of(&QProcess::finished); + QObject::connect( + process, signal, + [this, repo, fileMerge, backupPath, process, &numMergeFiles] { + qDebug() << "Merge Process Exited!"; + qDebug() << "Stdout: " << process->readAllStandardOutput(); + qDebug() << "Stderr: " << process->readAllStandardError(); + + QFileInfo merged(fileMerge.name); + QFileInfo backup(backupPath); + git::Config config = git::Config::global(); + bool modified = (merged.lastModified() > backup.lastModified()); + if (!modified || !config.value("mergetool.keepBackup")) + QFile::remove(backupPath); + + if (modified) { + int length = repo.workdir().path().length(); + repo.index().setStaged({fileMerge.name.mid(length + 1)}, true); + } + + if (--numMergeFiles == 0) { + deleteLater(); + } + }); + + QString fullFilePath = mRepo.workdir().filePath(fileMerge.name); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("LOCAL", local->fileName()); + env.insert("REMOTE", remote->fileName()); + env.insert("MERGED", fullFilePath); + env.insert("BASE", basePath); + process->setProcessEnvironment(env); #if defined(FLATPAK) || defined(DEBUG_FLATPAK) - QStringList arguments = {"--host", "--env=LOCAL=" + local->fileName(), - "--env=REMOTE=" + remote->fileName(), - "--env=MERGED=" + mFile, "--env=BASE=" + basePath}; - arguments.append("sh"); - arguments.append("-c"); - arguments.append(command); - // Debug("Command: " << "flatpak-spawn"); - process->start("flatpak-spawn", arguments); - // Debug("QProcess Arguments: " << process->arguments()); - if (!process->waitForStarted()) { - Debug("MergeTool starting failed"); - return false; - } + QStringList arguments = {"--host", "--env=LOCAL=" + local->fileName(), + "--env=REMOTE=" + remote->fileName(), + "--env=MERGED=" + fullFilePath, + "--env=BASE=" + basePath}; + arguments.append("sh"); + arguments.append("-c"); + arguments.append(command); + // Debug("Command: " << "flatpak-spawn"); + process->start("flatpak-spawn", arguments); + // Debug("QProcess Arguments: " << process->arguments()); + if (!process->waitForStarted()) { + Debug("MergeTool starting failed"); + return false; + } #else - QString bash = git::Command::bashPath(); - if (!bash.isEmpty()) { - process->start(bash, {"-c", command}); - } else if (!shell) { - process->start(git::Command::substitute(env, command)); - } else { - emit error(BashNotFound); - return false; - } + QString bash = git::Command::bashPath(); + if (!bash.isEmpty()) { + process->start(bash, {"-c", command}); + } else if (!shell) { + process->start(git::Command::substitute(env, command)); + } else { + emit error(BashNotFound); + return false; + } - if (!process->waitForStarted()) - return false; + if (!process->waitForStarted()) + return false; #endif + } // Detach from parent. setParent(nullptr); diff --git a/src/tools/MergeTool.h b/src/tools/MergeTool.h index a9212f67a..446d5987b 100644 --- a/src/tools/MergeTool.h +++ b/src/tools/MergeTool.h @@ -10,16 +10,23 @@ #ifndef MERGETOOL_H #define MERGETOOL_H +#include +#include #include "ExternalTool.h" #include "git/Blob.h" +class QObject; +namespace git { +class Diff; +class Repository; +}; // namespace git + class MergeTool : public ExternalTool { Q_OBJECT public: - MergeTool(const QString &file, const git::Blob &localBlob, - const git::Blob &remoteBlob, const git::Blob &baseBlob, - QObject *parent = nullptr); + MergeTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent); bool isValid() const override; @@ -29,9 +36,14 @@ class MergeTool : public ExternalTool { bool start() override; protected: - git::Blob mLocalBlob; - git::Blob mRemoteBlob; - git::Blob mBaseBlob; +private: + struct FileMerge { + QString name; + git::Blob local; + git::Blob remote; + git::Blob base; + }; + QVector mMerges; }; #endif diff --git a/src/tools/ShowTool.cpp b/src/tools/ShowTool.cpp index e2b503d14..1aabd52bf 100644 --- a/src/tools/ShowTool.cpp +++ b/src/tools/ShowTool.cpp @@ -95,24 +95,36 @@ bool ShowTool::openFileManager(QString path) { #endif } -ShowTool::ShowTool(const QString &file, QObject *parent) - : ExternalTool(file, parent) {} +ShowTool::ShowTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent) + : ExternalTool(files, diff, repo, parent) {} ExternalTool::Kind ShowTool::kind() const { return Show; } QString ShowTool::name() const { return tr("Show in %1").arg(tr(NAME)); } bool ShowTool::start() { + + foreach (const QString &file, mFiles) { #if defined(Q_OS_MAC) - return QProcess::startDetached( - "/usr/bin/osascript", {"-e", "tell application \"Finder\"", "-e", - QString("reveal POSIX file \"%1\"").arg(mFile), - "-e", "activate", "-e", "end tell"}); + if (!QProcess::startDetached("/usr/bin/osascript", + {"-e", "tell application \"Finder\"", "-e", + QString("reveal POSIX file \"%1\"").arg(file), + "-e", "activate", "-e", "end tell"})) { + return false; + } #elif defined(Q_OS_WIN) - return QProcess::startDetached("explorer.exe", - {"/select,", QDir::toNativeSeparators(mFile)}); + if (!QProcess::startDetached( + "explorer.exe", {"/select,", QDir::toNativeSeparators(file)})) { + return false; + } #else - QFileInfo info(mFile); - return openFileManager(info.isDir() ? info.filePath() : info.path()); + QFileInfo info(file); + if (!openFileManager(info.isDir() ? info.filePath() : info.path())) { + return false; + } #endif + } + + return true; } diff --git a/src/tools/ShowTool.h b/src/tools/ShowTool.h index b9624017a..68f756f32 100644 --- a/src/tools/ShowTool.h +++ b/src/tools/ShowTool.h @@ -12,13 +12,20 @@ #include "ExternalTool.h" +class QObject; +namespace git { +class Diff; +class Repository; +}; // namespace git + class ShowTool : public ExternalTool { Q_OBJECT public: static bool openFileManager(QString path); - ShowTool(const QString &file, QObject *parent = nullptr); + ShowTool(const QStringList &files, const git::Diff &diff, + const git::Repository &repo, QObject *parent); Kind kind() const override; QString name() const override; diff --git a/src/ui/DiffTreeModel.cpp b/src/ui/DiffTreeModel.cpp index a722705c6..a9f5de35d 100644 --- a/src/ui/DiffTreeModel.cpp +++ b/src/ui/DiffTreeModel.cpp @@ -7,6 +7,7 @@ // Author: Jason Haslam // +#include #include "DiffTreeModel.h" #include "conf/Settings.h" #include "git/Blob.h" @@ -16,15 +17,37 @@ #include "git/Patch.h" #include #include +#include namespace { const QString kLinkFmt = "%2"; +const std::array kModelHeaders = {QObject::tr("File Name"), + QObject::tr("Relative Path"), + QObject::tr("State")}; + +bool asList() { + return Settings::instance() + ->value(Setting::Id::ShowChangedFilesAsList, false) + .toBool(); +} + } // namespace DiffTreeModel::DiffTreeModel(const git::Repository &repo, QObject *parent) - : QAbstractItemModel(parent), mRepo(repo) {} + : QStandardItemModel(0, kModelHeaders.size(), parent), mRepo(repo), + mConstructed(false) { + // mConstructed only exists so that columnCount() returns the maximum number + // of columns when it is called internally by setHeaderData(). Without this, + // columnCount() will return 1 in the case that "List View" isn't selected and + // that means columns beyond the first won't be assigned the proper header + // value in the loop below. + for (int i = 0; i < kModelHeaders.size(); ++i) { + setHeaderData(i, Qt::Horizontal, kModelHeaders[i]); + } + mConstructed = true; +} DiffTreeModel::~DiffTreeModel() { delete mRoot; } @@ -91,7 +114,10 @@ int DiffTreeModel::rowCount(const QModelIndex &parent) const { return mDiff ? node(parent)->children().size() : 0; } -int DiffTreeModel::columnCount(const QModelIndex &parent) const { return 1; } +int DiffTreeModel::columnCount(const QModelIndex &parent) const { + return asList() || !mConstructed ? QStandardItemModel::columnCount(parent) + : 1; +} bool DiffTreeModel::hasChildren(const QModelIndex &parent) const { return mRoot && node(parent)->hasChildren(); @@ -134,7 +160,7 @@ void DiffTreeModel::modelIndices(const QModelIndex &parent, } for (int i = 0; i < n->children().length(); i++) { - auto child = createIndex(i, 0, n->children()[i]); + auto child = createIndex(i, parent.column(), n->children()[i]); if (recursive) modelIndices(child, list); else if (!node(child)->hasChildren()) @@ -195,9 +221,15 @@ QVariant DiffTreeModel::data(const QModelIndex &index, int role) const { return QVariant(); Node *node = this->node(index); + + // Skip intermediate path elements for trees showing file lists only. + if (node->hasChildren() && asList()) + return QVariant(); + switch (role) { - case Qt::DisplayRole: - return node->name(); + case Qt::DisplayRole: { + return getDisplayRole(index); + } // case Qt::DecorationRole: { // QFileInfo info(node->path()); @@ -212,7 +244,7 @@ QVariant DiffTreeModel::data(const QModelIndex &index, int role) const { return node->path(); case Qt::CheckStateRole: { - if (!mDiff.isValid() || !mDiff.isStatusDiff()) + if (!mDiff.isValid() || !mDiff.isStatusDiff() || index.column() > 0) return QVariant(); git::Index index = mDiff.index(); @@ -380,6 +412,22 @@ Node *DiffTreeModel::node(const QModelIndex &index) const { return index.isValid() ? static_cast(index.internalPointer()) : mRoot; } +QVariant DiffTreeModel::getDisplayRole(const QModelIndex &index) const { + Node *node = this->node(index); + if (asList()) { + QFileInfo fileInfo(node->path(true)); + switch (index.column()) { + case 0: + return fileInfo.fileName(); + case 1: + return fileInfo.path(); + default: + return ""; + } + } + return node->name(); +} + //############################################################################# //###### DiffTreeModel::Node ############################################## //############################################################################# diff --git a/src/ui/DiffTreeModel.h b/src/ui/DiffTreeModel.h index 9b1d2dba4..8cc14b6c7 100644 --- a/src/ui/DiffTreeModel.h +++ b/src/ui/DiffTreeModel.h @@ -14,7 +14,8 @@ #include "git/Index.h" #include "git/Tree.h" #include "git/Repository.h" -#include +#include +#include #include #include "git/Index.h" @@ -80,7 +81,7 @@ class Node : public QObject // item of the model * This Treemodel is similar to the normal tree model, but handles only the * files in the diff it self and not the complete tree */ -class DiffTreeModel : public QAbstractItemModel { +class DiffTreeModel : public QStandardItemModel { Q_OBJECT public: @@ -157,6 +158,7 @@ class DiffTreeModel : public QAbstractItemModel { private: Node *node(const QModelIndex &index) const; + QVariant getDisplayRole(const QModelIndex &index) const; QFileIconProvider mIconProvider; @@ -165,6 +167,7 @@ class DiffTreeModel : public QAbstractItemModel { git::Repository mRepo; bool mListView = false; + bool mConstructed = false; }; #endif /* DIFFTREEMODEL */ diff --git a/src/ui/DoubleTreeWidget.cpp b/src/ui/DoubleTreeWidget.cpp index c59746705..11bf91e23 100644 --- a/src/ui/DoubleTreeWidget.cpp +++ b/src/ui/DoubleTreeWidget.cpp @@ -15,7 +15,6 @@ #include "StatePushButton.h" #include "TreeProxy.h" #include "TreeView.h" -#include "ViewDelegate.h" #include "Debug.h" #include "conf/Settings.h" #include "DiffView/DiffView.h" @@ -98,6 +97,7 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent) listView->setChecked(Settings::instance() ->value(Setting::Id::ShowChangedFilesAsList, false) .toBool()); + RepoView::parentView(this)->refresh(); connect(listView, &QAction::triggered, this, [this](bool checked) { Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList, checked); @@ -160,13 +160,8 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent) repoView->updateSubmodules(submodules, recursive, init, force_checkout); }); - TreeProxy *treewrapperStaged = new TreeProxy(true, this); - treewrapperStaged->setSourceModel(mDiffTreeModel); - stagedFiles->setModel(treewrapperStaged); - stagedFiles->setHeaderHidden(true); - ViewDelegate *stagedDelegate = new ViewDelegate(); - stagedDelegate->setDrawArrow(false); - stagedFiles->setItemDelegateForColumn(0, stagedDelegate); + + stagedFiles->setModel(new TreeProxy(true, mDiffTreeModel, this)); QHBoxLayout *hBoxLayout = new QHBoxLayout(); QLabel *label = new QLabel(kStagedFiles); @@ -192,13 +187,7 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent) showFileContextMenu(pos, repoView, unstagedFiles, false); }); - TreeProxy *treewrapperUnstaged = new TreeProxy(false, this); - treewrapperUnstaged->setSourceModel(mDiffTreeModel); - unstagedFiles->setModel(treewrapperUnstaged); - unstagedFiles->setHeaderHidden(true); - ViewDelegate *unstagedDelegate = new ViewDelegate(); - unstagedDelegate->setDrawArrow(false); - unstagedFiles->setItemDelegateForColumn(0, unstagedDelegate); + unstagedFiles->setModel(new TreeProxy(false, mDiffTreeModel, this)); hBoxLayout = new QHBoxLayout(); mUnstagedCommitedFiles = new QLabel(kUnstagedFiles); @@ -318,46 +307,24 @@ QModelIndex DoubleTreeWidget::selectedIndex() const { return QModelIndex(); } -static void addNodeToMenu(const git::Index &index, QStringList &files, - const Node *node, bool staged, bool statusDiff) { - Debug("DoubleTreeWidgetr addNodeToMenu()" << node->name()); - - if (node->hasChildren()) { - for (auto child : node->children()) { - addNodeToMenu(index, files, child, staged, statusDiff); - } - - } else { - auto path = node->path(true); - - auto stageState = index.isStaged(path); - - if ((staged && stageState != git::Index::Unstaged) || - (!staged && stageState != git::Index::Staged) || !statusDiff) { - files.append(path); - } - } -} - void DoubleTreeWidget::showFileContextMenu(const QPoint &pos, RepoView *view, QTreeView *tree, bool staged) { - QStringList files; - QModelIndexList indexes = tree->selectionModel()->selectedIndexes(); + QModelIndexList modelIndexes = tree->selectionModel()->selectedIndexes(); const auto diff = view->diff(); if (!diff.isValid()) return; - const bool statusDiff = diff.isStatusDiff(); - foreach (const QModelIndex &index, indexes) { - auto node = index.data(Qt::UserRole).value(); - - addNodeToMenu(view->repo().index(), files, node, staged, statusDiff); + const git::Index repoIndex = view->repo().index(); + AccumRepoFiles accumulatedFiles(staged, diff.isStatusDiff()); + foreach (const QModelIndex &modelIndex, modelIndexes) { + auto node = modelIndex.data(Qt::UserRole).value(); + accumulatedFiles.add(repoIndex, node); } - if (files.isEmpty()) + if (accumulatedFiles.getAllFiles().isEmpty()) return; - auto menu = new FileContextMenu(view, files, git::Index(), tree); + auto menu = new FileContextMenu(view, accumulatedFiles, git::Index(), tree); menu->setAttribute(Qt::WA_DeleteOnClose); menu->popup(tree->mapToGlobal(pos)); } diff --git a/src/ui/FileContextMenu.cpp b/src/ui/FileContextMenu.cpp index b7e184b24..314aa3f67 100644 --- a/src/ui/FileContextMenu.cpp +++ b/src/ui/FileContextMenu.cpp @@ -9,6 +9,7 @@ #include "FileContextMenu.h" #include "RepoView.h" +#include "DiffTreeModel.h" #include "IgnoreDialog.h" #include "conf/Settings.h" #include "Debug.h" @@ -19,6 +20,8 @@ #include "host/Repository.h" #include "tools/EditTool.h" #include "tools/ShowTool.h" +#include "tools/DiffTool.h" +#include "tools/MergeTool.h" #include #include #include @@ -86,61 +89,25 @@ void handlePath(const git::Repository &repo, const QString &path, } // namespace -FileContextMenu::FileContextMenu(RepoView *view, const QStringList &files, +FileContextMenu::FileContextMenu(RepoView *view, + const AccumRepoFiles &accumFiles, const git::Index &index, QWidget *parent) - : QMenu(parent), mView(view), mFiles(files) { - // Show diff and merge tools for the currently selected diff. + : QMenu(parent), mView(view), mAccumFiles(accumFiles) { git::Diff diff = view->diff(); - git::Repository repo = view->repo(); - if (!diff.isValid()) return; - // Create external tools. - QList showTools; - QList editTools; - QList diffTools; - QList mergeTools; - foreach (const QString &file, files) { - // Convert to absolute path. - QString path = repo.workdir().filePath(file); - - // Add show tool. - showTools.append(new ShowTool(path, this)); - - // Add edit tool. - editTools.append(new EditTool(path, this)); - - // Add diff or merge tool. - if (ExternalTool *tool = ExternalTool::create(file, diff, repo, this)) { - switch (tool->kind()) { - case ExternalTool::Diff: - diffTools.append(tool); - break; - - case ExternalTool::Merge: - mergeTools.append(tool); - break; - - case ExternalTool::Show: // fall through - case ExternalTool::Edit: - Q_ASSERT(false); - break; - } - - connect(tool, &ExternalTool::error, [this](ExternalTool::Error error) { - if (error != ExternalTool::BashNotFound) - return; + git::Repository repo = view->repo(); - QString title = tr("Bash Not Found"); - QString text = tr("Bash was not found on your PATH."); - QMessageBox msg(QMessageBox::Warning, title, text, QMessageBox::Ok, - this); - msg.setInformativeText( - tr("Bash is required to execute external tools.")); - msg.exec(); - }); - } + // Create external tools. + const QStringList &files = mAccumFiles.getFiles(); + QList showTools, editTools, diffTools, mergeTools; + attachTool(new ShowTool(files, diff, repo, this), showTools); + attachTool(new EditTool(files, diff, repo, this), editTools); + if (diff.isConflicted()) { + attachTool(new MergeTool(files, diff, repo, this), mergeTools); + } else { + attachTool(new DiffTool(files, diff, repo, this), diffTools); } // Add external tool actions. @@ -173,8 +140,10 @@ FileContextMenu::FileContextMenu(RepoView *view, const QStringList &files, } } - addAction(locked ? tr("Unlock") : tr("Lock"), - [view, files, locked] { view->lfsSetLocked(files, !locked); }); + const QStringList &allFiles = files; + addAction(locked ? tr("Unlock") : tr("Lock"), [view, allFiles, locked] { + view->lfsSetLocked(allFiles, !locked); + }); } // Add single selection actions. @@ -473,10 +442,11 @@ void FileContextMenu::handleCommits(const QList &commits, } void FileContextMenu::ignoreFile() { - if (!mFiles.count()) + const QStringList &files = mAccumFiles.getFilesInDirs(); + if (!files.count()) return; - auto d = new IgnoreDialog(mFiles.join('\n'), parentWidget()); + auto d = new IgnoreDialog(files.join('\n'), parentWidget()); d->setAttribute(Qt::WA_DeleteOnClose); auto *view = mView; @@ -549,3 +519,100 @@ void FileContextMenu::addExternalToolsAction( } } } + +void FileContextMenu::attachTool(ExternalTool *tool, + QList &tools) { + tools.append(tool); + + connect(tool, &ExternalTool::error, [this](ExternalTool::Error error) { + if (error != ExternalTool::BashNotFound) + return; + + QString title = tr("Bash Not Found"); + QString text = tr("Bash was not found on your PATH."); + QMessageBox msg(QMessageBox::Warning, title, text, QMessageBox::Ok, this); + msg.setInformativeText(tr("Bash is required to execute external tools.")); + msg.exec(); + }); +} + +AccumRepoFiles::AccumRepoFiles(const QString &file) { mFiles.append(file); } + +AccumRepoFiles::AccumRepoFiles(const QStringList &files) { + mFiles.append(files); +} + +AccumRepoFiles::AccumRepoFiles(bool staged, bool statusDiff) + : mStaged(staged), mStatusDiff(statusDiff) {} + +// Get all files that were accumulated individually, not as part of a +// directory being accumulated. The files contain the file name with any +// path provided (relative or absolute). +const QStringList &AccumRepoFiles::getFiles() const { return mFiles; } + +// Get all directory paths that we accumulated. +QStringList AccumRepoFiles::getAccumulatedDirs() const { + return mFilesInDirMap.keys(); +} + +// Get a list of files from all accumulated directories or just those from +// the requested ones. +QStringList AccumRepoFiles::getFilesInDirs(const QStringList &dirs) const { + QStringList filesInDirs; + + const QStringList &dirKeys = dirs.isEmpty() ? getAccumulatedDirs() : dirs; + for (QString dirKey : dirKeys) { + ConstQMapIterator iter = mFilesInDirMap.find(dirKey); + if (iter == mFilesInDirMap.end()) + continue; + filesInDirs.append(iter.value()); + } + + return filesInDirs; +} + +// Get a list of all accumulated files with paths. These include those +// accumulated individually as well as those that were added as part of +// accumulating a directory. +QStringList AccumRepoFiles::getAllFiles() const { + return getFiles() + getFilesInDirs(); +} + +// Accumulate the given node as either an individual file or as a directory. +// For directories, all the files in the directory will be enumeracted and +// accumulated, recursively. +void AccumRepoFiles::add(const git::Index &index, const Node *node) { + Debug("AccumRepoFiles accumulateFiles() " << node->name()); + + addToFileList(mFiles, index, node); +} + +void AccumRepoFiles::addDirFiles(const git::Index &index, const Node *node) { + Debug("AccumRepoFiles addDirFiles() " << node->name()); + + for (auto childNode : node->children()) { + QFileInfo fileInfo = childNode->path(true); + addToFileList(mFilesInDirMap[fileInfo.path()], index, childNode); + } +} + +void AccumRepoFiles::addToFileList(QStringList &files, const git::Index &index, + const Node *node) { + if (node->hasChildren()) { + addDirFiles(index, node); + } else { + addFile(files, index, node); + } +} + +void AccumRepoFiles::addFile(QStringList &fileList, const git::Index &index, + const Node *node) const { + auto path = node->path(true); + + git::Index::StagedState stageState = index.isStaged(path); + + if ((mStaged && stageState != git::Index::Unstaged) || + (!mStaged && stageState != git::Index::Staged) || !mStatusDiff) { + fileList.append(path); + } +} diff --git a/src/ui/FileContextMenu.h b/src/ui/FileContextMenu.h index 8d89543fd..f9c959dd6 100644 --- a/src/ui/FileContextMenu.h +++ b/src/ui/FileContextMenu.h @@ -14,15 +14,67 @@ #include "git/Index.h" #include "git/Commit.h" #include +#include class ExternalTool; class RepoView; +class Node; + +class AccumRepoFiles { +public: + AccumRepoFiles(bool staged = false, bool statusDiff = false); + AccumRepoFiles(const QString &file); + AccumRepoFiles(const QStringList &files); + AccumRepoFiles(const AccumRepoFiles &) = default; + AccumRepoFiles &operator=(const AccumRepoFiles &) = default; + ~AccumRepoFiles() = default; + + // Get all files that were accumulated individually, not as part of a + // directory being accumulated. The files contain the file name with any + // path provided (relative or absolute). + const QStringList &getFiles() const; + + // Get all directory paths that we accumulated. + QStringList getAccumulatedDirs() const; + + // Get a list of files from all accumulated directories or just those from + // the requested ones. + QStringList getFilesInDirs(const QStringList &dirs = {}) const; + + // Get a list of all accumulated files with paths. These include those + // accumulated individually as well as those that were added as part of + // accumulating a directory. + QStringList getAllFiles() const; + + // Accumulate the given node as either an individual file or as a directory. + // For directories, all the files in the directory will be enumeracted and + // accumulated, recursively. + void add(const git::Index &index, const Node *node); + +private: + void addToFileList(QStringList &files, const git::Index &index, + const Node *node); + void addDirFiles(const git::Index &index, const Node *node); + void addFile(QStringList &files, const git::Index &index, + const Node *node) const; + + AccumRepoFiles() = delete; + AccumRepoFiles(AccumRepoFiles &&) = delete; + AccumRepoFiles(const AccumRepoFiles &&) = delete; + AccumRepoFiles &&operator=(const AccumRepoFiles &&) = delete; + + typedef QMap::const_iterator ConstQMapIterator; + QMap mFilesInDirMap; + QStringList mFiles; + bool mStaged = false; + bool mStatusDiff = false; +}; class FileContextMenu : public QMenu { Q_OBJECT public: - FileContextMenu(RepoView *view, const QStringList &files, + FileContextMenu(RepoView *view, const AccumRepoFiles &accumFiles, const git::Index &index = git::Index(), QWidget *parent = nullptr); private slots: @@ -36,11 +88,11 @@ private slots: const QStringList &files); void handleCommits(const QList &commits, const QStringList &files); + void attachTool(ExternalTool *tool, QList &tools); RepoView *mView; - QStringList mFiles; + AccumRepoFiles mAccumFiles; friend class TestTreeView; }; - #endif diff --git a/src/ui/TreeProxy.cpp b/src/ui/TreeProxy.cpp index f956c3a6b..2b447f855 100644 --- a/src/ui/TreeProxy.cpp +++ b/src/ui/TreeProxy.cpp @@ -23,8 +23,10 @@ const QString kLinkFmt = "%2"; } // namespace -TreeProxy::TreeProxy(bool staged, QObject *parent) - : QSortFilterProxyModel(parent), mStaged(staged) {} +TreeProxy::TreeProxy(bool staged, QAbstractItemModel *model, QObject *parent) + : mStaged(staged), QSortFilterProxyModel(parent) { + setSourceModel(model); +} TreeProxy::~TreeProxy() {} diff --git a/src/ui/TreeProxy.h b/src/ui/TreeProxy.h index 2e8d75ae2..93ab7a4fc 100644 --- a/src/ui/TreeProxy.h +++ b/src/ui/TreeProxy.h @@ -16,13 +16,14 @@ #include #include +class QAbstractItemModel; class TreeModel; class TreeProxy : public QSortFilterProxyModel { Q_OBJECT public: - TreeProxy(bool staged, QObject *parent = nullptr); + TreeProxy(bool staged, QAbstractItemModel *model, QObject *parent); virtual ~TreeProxy(); bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole, bool ignoreIndexChanges = false); @@ -30,6 +31,10 @@ class TreeProxy : public QSortFilterProxyModel { void enableFilter(bool enable) { mFilter = enable; } + int columnCount(const QModelIndex &parent = QModelIndex()) const override { + return sourceModel()->columnCount(); + } + private: using QSortFilterProxyModel::setData; bool filterAcceptsRow(int source_row, diff --git a/src/ui/TreeView.cpp b/src/ui/TreeView.cpp index a7895dd18..aa38d506c 100644 --- a/src/ui/TreeView.cpp +++ b/src/ui/TreeView.cpp @@ -24,6 +24,9 @@ #include "RepoView.h" #include #include +#include "conf/Settings.h" +#include +#include #ifdef Q_OS_WIN #define ICON_SIZE 48 @@ -41,8 +44,29 @@ const QString kLabelFmt = "

%1

"; } // namespace TreeView::TreeView(QWidget *parent, const QString &name) - : QTreeView(parent), mSharedDelegate(new ViewDelegate(this)), mName(name) { + : QTreeView(parent), mDelegateCol(0), + mFileListDelegatePtr(std::make_unique(this, true)), + mFileTreeDelegatePtr(std::make_unique(this)), mName(name) { setObjectName(name); + connect(RepoView::parentView(this)->repo().notifier(), + &git::RepositoryNotifier::referenceUpdated, this, + &TreeView::updateView); +} + +void TreeView::updateView() { + QAbstractItemModel *itemModel = model(); + if (!itemModel) + return; + + // Remove any previous delegate on the current column, get the new current + // column, and set the delegate on that. + setItemDelegateForColumn(mDelegateCol, nullptr); + mDelegateCol = itemModel->columnCount() - 1; + setItemDelegateForColumn(mDelegateCol, mDelegateCol + ? mFileListDelegatePtr.get() + : mFileTreeDelegatePtr.get()); + + setHeaderHidden(mDelegateCol ? false : true); } void TreeView::setModel(QAbstractItemModel *model) { @@ -57,6 +81,10 @@ void TreeView::setModel(QAbstractItemModel *model) { connect(model, &QAbstractItemModel::rowsInserted, this, QOverload::of( &TreeView::updateCollapseCount)); + + // Allow column sorting and set the default column and sort order. + setSortingEnabled(true); + sortByColumn(0, Qt::AscendingOrder); } void TreeView::discard(const QModelIndex &index, const bool force) { diff --git a/src/ui/TreeView.h b/src/ui/TreeView.h index 138fd34e0..87cc642f1 100644 --- a/src/ui/TreeView.h +++ b/src/ui/TreeView.h @@ -11,6 +11,8 @@ #define TREEVIEW_H #include +#include +#include "ViewDelegate.h" class QItemDelegate; class DiffTreeModel; @@ -96,9 +98,12 @@ public slots: bool suppressDeselectionHandling{false}; int mCollapseCount; // Counts the number of collapsed folders. bool mSupressItemExpandStateChanged{false}; + void updateView(); - QItemDelegate *mSharedDelegate; QString mName; + std::unique_ptr mFileListDelegatePtr; + std::unique_ptr mFileTreeDelegatePtr; + int mDelegateCol = 0; }; #endif // TREEVIEW_H diff --git a/src/ui/ViewDelegate.cpp b/src/ui/ViewDelegate.cpp index 194a915a8..8e69662ef 100644 --- a/src/ui/ViewDelegate.cpp +++ b/src/ui/ViewDelegate.cpp @@ -10,37 +10,6 @@ void ViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, QStyleOptionViewItem opt = option; drawBackground(painter, opt, index); - // Draw >. - if (mDrawArrow && index.model()->hasChildren(index)) { - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - - QColor color = opt.palette.color(QPalette::Active, QPalette::BrightText); - if (opt.state & QStyle::State_Selected) - color = - !opt.showDecorationSelected - ? opt.palette.color(QPalette::Active, QPalette::WindowText) - : opt.palette.color(QPalette::Active, QPalette::HighlightedText); - - painter->setPen(color); - painter->setBrush(color); - - int x = opt.rect.x() + opt.rect.width() - 3; - int y = opt.rect.y() + (opt.rect.height() / 2); - - QPainterPath path; - path.moveTo(x, y); - path.lineTo(x - 5, y - 3); - path.lineTo(x - 5, y + 3); - path.closeSubpath(); - painter->drawPath(path); - - painter->restore(); - - // Adjust rect to exclude the arrow. - opt.rect.adjust(0, 0, -11, 0); - } - // Draw badges. QString status = index.data(TreeModel::StatusRole).toString(); if (!status.isEmpty()) { @@ -49,19 +18,30 @@ void ViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, int width = size.width(); int height = size.height(); + auto startIter = status.cbegin(), endIter = status.cend(); + int leftAdjust = 0, rightAdjust = -3, leftWidth = 0, rightWidth = -width; + if (mMultiColumn) { + leftAdjust = 3; + rightAdjust = 0; + leftWidth = width; + rightWidth = 0; + std::reverse(status.begin(), status.end()); + } + // Add extra space. - opt.rect.adjust(0, 0, -3, 0); + opt.rect.adjust(leftAdjust, 0, rightAdjust, 0); for (int i = 0; i < status.count(); ++i) { int x = opt.rect.x() + opt.rect.width(); int y = opt.rect.y() + (opt.rect.height() / 2); - QRect rect(x - width, y - (height / 2), width, height); + QRect rect(mMultiColumn ? opt.rect.x() : x - width, y - (height / 2), + width, height); Badge::paint(painter, {Badge::Label(Badge::Label::Type::Status, status.at(i))}, rect, &opt); // Adjust rect. - opt.rect.adjust(0, 0, -width - 3, 0); + opt.rect.adjust(leftWidth + leftAdjust, 0, rightWidth + rightAdjust, 0); } } diff --git a/src/ui/ViewDelegate.h b/src/ui/ViewDelegate.h index 03f168a08..fc81c09ae 100644 --- a/src/ui/ViewDelegate.h +++ b/src/ui/ViewDelegate.h @@ -9,9 +9,8 @@ */ class ViewDelegate : public QItemDelegate { public: - ViewDelegate(QObject *parent = nullptr) : QItemDelegate(parent) {} - - void setDrawArrow(bool enable) { mDrawArrow = enable; } + ViewDelegate(QObject *parent, bool multiColumn = false) + : QItemDelegate(parent), mMultiColumn(multiColumn) {} void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; @@ -20,7 +19,7 @@ class ViewDelegate : public QItemDelegate { const QModelIndex &index) const override; private: - bool mDrawArrow = true; + bool mMultiColumn = false; }; #endif // VIEWDELEGATE_H diff --git a/test/TreeView.cpp b/test/TreeView.cpp index 246e8efbf..872299f83 100644 --- a/test/TreeView.cpp +++ b/test/TreeView.cpp @@ -3,7 +3,9 @@ #include "ui/MainWindow.h" #include "ui/DoubleTreeWidget.h" #include "ui/TreeView.h" +#include "ui/TreeProxy.h" #include "ui/FileContextMenu.h" +#include "conf/Settings.h" #include @@ -22,6 +24,18 @@ using namespace QTest; \ RepoView *repoView = window.currentView(); +static void disableListView(TreeView &treeView, RepoView &repoView) { + auto treeProxy = dynamic_cast(treeView.model()); + QVERIFY(treeProxy); + + auto diffTreeModel = dynamic_cast(treeProxy->sourceModel()); + QVERIFY(diffTreeModel); + + diffTreeModel->enableListView(false); + Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList, false); + repoView.refresh(); +} + class TestTreeView : public QObject { Q_OBJECT @@ -46,6 +60,7 @@ void TestTreeView::restoreStagedFileAfterCommit() { { auto unstagedTree = doubleTree->findChild("Unstaged"); QVERIFY(unstagedTree); + disableListView(*unstagedTree, *view); QAbstractItemModel *unstagedModel = unstagedTree->model(); // Wait for refresh auto timeout = Timeout(10000, "Repository didn't refresh in time"); @@ -179,10 +194,11 @@ void TestTreeView::discardFiles() { auto *menu = doubleTree->findChild(); QVERIFY(menu); - QCOMPARE(menu->mFiles.count(), 1); + auto files = menu->mAccumFiles.getFiles(); + QCOMPARE(files.count(), 1); // only folder1/file.txt shall get discarded. // folder1/file2.txt shall not discarded! - QCOMPARE(menu->mFiles.at(0), "folder1/file.txt"); + QCOMPARE(files.at(0), "folder1/file.txt"); // From here on everything is tested in TestFileContextMenu } diff --git a/test/index.cpp b/test/index.cpp index c6592afe6..f3fdc3c80 100644 --- a/test/index.cpp +++ b/test/index.cpp @@ -13,6 +13,8 @@ #include "ui/MainWindow.h" #include "ui/RepoView.h" #include "ui/TreeView.h" +#include "ui/TreeProxy.h" +#include "conf/Settings.h" #include #include #include @@ -20,6 +22,18 @@ using namespace Test; using namespace QTest; +static void disableListView(TreeView &treeView, RepoView &repoView) { + auto treeProxy = dynamic_cast(treeView.model()); + QVERIFY(treeProxy); + + auto diffTreeModel = dynamic_cast(treeProxy->sourceModel()); + QVERIFY(diffTreeModel); + + diffTreeModel->enableListView(false); + Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList, false); + repoView.refresh(); +} + class TestIndex : public QObject { Q_OBJECT @@ -56,6 +70,8 @@ void TestIndex::stageAddition() { auto unstagedFiles = doubleTree->findChild("Unstaged"); QVERIFY(unstagedFiles); + disableListView(*unstagedFiles, *view); + auto stagedFiles = doubleTree->findChild("Staged"); QVERIFY(stagedFiles); @@ -151,6 +167,8 @@ void TestIndex::stageDirectory() { auto unstagedFiles = doubleTree->findChild("Unstaged"); QVERIFY(unstagedFiles); + disableListView(*unstagedFiles, *view); + auto stagedFiles = doubleTree->findChild("Staged"); QVERIFY(stagedFiles);