From 64952cb511911afa4bbe2363cfa7e372aaa47e29 Mon Sep 17 00:00:00 2001 From: Renaud Guezennec Date: Thu, 18 Jul 2024 18:33:33 +0200 Subject: [PATCH] Git: Colorize modified files in projects view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: QTCREATORBUG-8857 Change-Id: I9922f731cf3c7a7f25a72cbe6eab64391f4f8054 Reviewed-by: André Hartmann Reviewed-by: Orgad Shaneh --- src/plugins/coreplugin/iversioncontrol.cpp | 16 ++++ src/plugins/coreplugin/iversioncontrol.h | 17 ++++ src/plugins/git/gitclient.cpp | 92 +++++++++++++++++++ src/plugins/git/gitclient.h | 13 +++ src/plugins/git/gitplugin.cpp | 24 +++++ src/plugins/git/gitplugin.h | 1 + src/plugins/projectexplorer/projectmodels.cpp | 29 ++++++ src/plugins/projectexplorer/projectmodels.h | 1 + src/plugins/projectexplorer/projectnodes.cpp | 16 ++++ src/plugins/projectexplorer/projectnodes.h | 2 + 10 files changed, 211 insertions(+) diff --git a/src/plugins/coreplugin/iversioncontrol.cpp b/src/plugins/coreplugin/iversioncontrol.cpp index 3ff7de5bcdd..78925435acb 100644 --- a/src/plugins/coreplugin/iversioncontrol.cpp +++ b/src/plugins/coreplugin/iversioncontrol.cpp @@ -63,6 +63,22 @@ FilePaths IVersionControl::additionalToolsPath() const return {}; } +bool IVersionControl::hasModification(const FilePath &path) const +{ + Q_UNUSED(path) + return false; +} + +void IVersionControl::monitorDirectory(const Utils::FilePath &path) +{ + Q_UNUSED(path) +} + +void IVersionControl::stopMonitoringDirectory(const Utils::FilePath &path) +{ + Q_UNUSED(path) +} + IVersionControl::RepoUrl::RepoUrl(const QString &location) { if (location.isEmpty()) diff --git a/src/plugins/coreplugin/iversioncontrol.h b/src/plugins/coreplugin/iversioncontrol.h index 31e35437770..378efbc3870 100644 --- a/src/plugins/coreplugin/iversioncontrol.h +++ b/src/plugins/coreplugin/iversioncontrol.h @@ -92,6 +92,22 @@ public: * Returns true is the VCS is configured to run. */ virtual bool isConfigured() const = 0; + + /*! + * Returns true is the file has modification compare to version control + */ + virtual bool hasModification(const Utils::FilePath &path) const; + + /*! + * Starts monitoring modified files inside path + */ + virtual void monitorDirectory(const Utils::FilePath &path); + + /*! + * Stops monitoring modified files inside path + */ + virtual void stopMonitoringDirectory(const Utils::FilePath &path); + /*! * Called to query whether a VCS supports the respective operations. * @@ -207,6 +223,7 @@ public: signals: void repositoryChanged(const Utils::FilePath &repository); void filesChanged(const QStringList &files); + void updateFileStatus(const Utils::FilePath &repository, const QStringList &files); void configurationChanged(); private: diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index 3e4796b81b2..e1c75e4d9fa 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -137,6 +137,17 @@ static void stage(DiffEditorController *diffController, const QString &patch, bo } } +/////////////////////////////// + +static QList submoduleDataToAbsolutePath(const SubmoduleDataMap &submodules, + const FilePath &rootDir) +{ + QList res; + std::transform(std::begin(submodules), std::end(submodules), std::back_inserter(res), + [rootDir](const SubmoduleData &data) { return rootDir.pathAppended(data.dir); }); + return res; +} + class GitBaseDiffEditorController : public VcsBaseDiffEditorController { Q_OBJECT @@ -822,10 +833,14 @@ GitClient &gitClient() GitClient::GitClient() : VcsBase::VcsBaseClientImpl(&Internal::settings()) + , m_timer(new QTimer) { m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2") .arg(QCoreApplication::applicationFilePath()) .arg(QCoreApplication::applicationPid()); + + connect(m_timer.get(), &QTimer::timeout, this, &GitClient::updateModificationInfos); + m_timer->setInterval(10000); // 10s } GitClient::~GitClient() = default; @@ -899,6 +914,83 @@ FilePaths GitClient::unmanagedFiles(const FilePaths &filePaths) const return res; } +bool GitClient::hasModification(const Utils::FilePath &workingDirectory, + const Utils::FilePath &fileName) const +{ + const ModificationInfo &info = m_modifInfos[workingDirectory]; + QString fileNameFromRoot = fileName.absoluteFilePath().toString(); + int length = workingDirectory.toString().size(); + fileNameFromRoot.remove(0, length + 1); + return info.modifiedFiles.contains(fileNameFromRoot); +} + +void GitClient::stopMonitoring(const Utils::FilePath &path) +{ + const FilePath directory = path; + // Submodule management + QList subPaths = submoduleDataToAbsolutePath(submoduleList(directory), directory); + std::for_each(std::begin(subPaths), std::end(subPaths), + [this](const FilePath &subModule) { m_modifInfos.remove(subModule); }); + m_modifInfos.remove(directory); + if (m_modifInfos.isEmpty()) + m_timer->stop(); +} + +void GitClient::monitorDirectory(const Utils::FilePath &path) +{ + const FilePath directory = path; + if (directory.isEmpty()) + return; + m_modifInfos.insert(directory, {directory, {}}); + // Submodule management + QList subPaths = submoduleDataToAbsolutePath(submoduleList(directory), directory); + std::for_each(std::begin(subPaths), std::end(subPaths), [this](const FilePath &subModule) { + m_modifInfos.insert(subModule, {subModule, {}}); + }); + if (!m_timer->isActive()) + m_timer->start(); + updateModificationInfos(); +} + +void GitClient::updateModificationInfos() +{ + for (ModificationInfo &info : m_modifInfos) { + const FilePath &path = info.rootPath; + const auto command = [&info](const CommandResult &result){ + const QStringList res = result.cleanedStdOut().split("\n", Qt::SkipEmptyParts); + QSet modifiedFiles; + for (const QString &line : res) + { + if (line.size() <= 3) + continue; + + QString file = line; + // can't use stateFor() function from commitdata.cpp + static const QSet gitStates{'M', 'A'}; + + if (gitStates.contains(line.at(0)) || gitStates.contains(line.at(1))) + modifiedFiles.insert(file.remove(0, 3).trimmed()); + } + + const QSet oldfiles = info.modifiedFiles; + info.modifiedFiles = modifiedFiles; + + QStringList newList = modifiedFiles.values(); + QStringList list = oldfiles.values(); + std::sort(std::begin(list), std::end(list)); + std::sort(std::begin(newList), std::end(newList)); + QStringList statusChangedFiles; + + std::set_symmetric_difference(std::begin(list), std::end(list), + std::begin(newList), std::end(newList), + std::back_inserter(statusChangedFiles)); + + emitFileStatusChanged(info.rootPath, statusChangedFiles); + }; + vcsExecWithHandler(path, {"status", "-s", "--porcelain"}, this, command, RunFlags::NoOutput); + } +} + QTextCodec *GitClient::defaultCommitEncoding() const { // Set default commit encoding to 'UTF-8', when it's not set, diff --git a/src/plugins/git/gitclient.h b/src/plugins/git/gitclient.h index b08e45da63e..ca67eca31f6 100644 --- a/src/plugins/git/gitclient.h +++ b/src/plugins/git/gitclient.h @@ -109,6 +109,12 @@ public: PushAction m_pushAction = NoPush; }; + struct ModificationInfo + { + Utils::FilePath rootPath; + QSet modifiedFiles; + }; + GitClient(); ~GitClient(); @@ -124,6 +130,10 @@ public: Utils::FilePath findGitDirForRepository(const Utils::FilePath &repositoryDir) const; bool managesFile(const Utils::FilePath &workingDirectory, const QString &fileName) const; Utils::FilePaths unmanagedFiles(const Utils::FilePaths &filePaths) const; + bool hasModification(const Utils::FilePath &workingDirectory, + const Utils::FilePath &fileName) const; + void monitorDirectory(const Utils::FilePath &path); + void stopMonitoring(const Utils::FilePath &path); void diffFile(const Utils::FilePath &workingDirectory, const QString &fileName) const; void diffFiles(const Utils::FilePath &workingDirectory, @@ -373,6 +383,7 @@ private: const Utils::FilePath &oldGitBinDir) const; bool cleanList(const Utils::FilePath &workingDirectory, const QString &modulePath, const QString &flag, QStringList *files, QString *errorMessage); + void updateModificationInfos(); enum ContinueCommandMode { ContinueOnly, @@ -390,6 +401,8 @@ private: QString m_gitQtcEditor; QMap m_stashInfo; + QHash m_modifInfos; + std::unique_ptr m_timer; QString m_diffCommit; Utils::FilePaths m_updatedSubmodules; bool m_disableEditor = false; diff --git a/src/plugins/git/gitplugin.cpp b/src/plugins/git/gitplugin.cpp index 044310d3862..cd72dae1682 100644 --- a/src/plugins/git/gitplugin.cpp +++ b/src/plugins/git/gitplugin.cpp @@ -155,6 +155,9 @@ public: FilePaths unmanagedFiles(const FilePaths &filePaths) const final; bool isConfigured() const final; + bool hasModification(const Utils::FilePath &path) const final; + void monitorDirectory(const Utils::FilePath &path) final; + void stopMonitoringDirectory(const Utils::FilePath &path) final; bool supportsOperation(Operation operation) const final; bool vcsOpen(const FilePath &filePath) final; bool vcsAdd(const FilePath &filePath) final; @@ -1714,6 +1717,22 @@ bool GitPluginPrivate::isConfigured() const return !gitClient().vcsBinary({}).isEmpty(); } +bool GitPluginPrivate::hasModification(const Utils::FilePath &path) const +{ + const Utils::FilePath projectDir = gitClient().findRepositoryForDirectory(path.absolutePath()); + return gitClient().hasModification(projectDir, path); +} + +void GitPluginPrivate::monitorDirectory(const Utils::FilePath &path) +{ + gitClient().monitorDirectory(gitClient().findRepositoryForDirectory(path)); +} + +void GitPluginPrivate::stopMonitoringDirectory(const Utils::FilePath &path) +{ + gitClient().stopMonitoring(gitClient().findRepositoryForDirectory(path)); +} + bool GitPluginPrivate::supportsOperation(Operation operation) const { if (!isConfigured()) @@ -1830,6 +1849,11 @@ void emitRepositoryChanged(const FilePath &r) emit dd->repositoryChanged(r); } +void emitFileStatusChanged(const FilePath &r, const QStringList &l) +{ + emit dd->updateFileStatus(r, l); +} + void startRebaseFromCommit(const FilePath &workingDirectory, const QString &commit) { dd->startRebaseFromCommit(workingDirectory, commit); diff --git a/src/plugins/git/gitplugin.h b/src/plugins/git/gitplugin.h index c61f98aab0c..a6658fbf8d4 100644 --- a/src/plugins/git/gitplugin.h +++ b/src/plugins/git/gitplugin.h @@ -22,6 +22,7 @@ bool isCommitEditorOpen(); void emitFilesChanged(const QStringList &); void emitRepositoryChanged(const Utils::FilePath &); +void emitFileStatusChanged(const Utils::FilePath &repository, const QStringList &files); void startRebaseFromCommit(const Utils::FilePath &workingDirectory, const QString &commit); void manageRemotes(); void initRepository(); diff --git a/src/plugins/projectexplorer/projectmodels.cpp b/src/plugins/projectexplorer/projectmodels.cpp index b7be5e1546b..6c804a6f2bc 100644 --- a/src/plugins/projectexplorer/projectmodels.cpp +++ b/src/plugins/projectexplorer/projectmodels.cpp @@ -241,6 +241,8 @@ QVariant FlatModel::data(const QModelIndex &index, int role) const return font; } case Qt::ForegroundRole: + if (fileNode && fileNode->hasModification()) + return Utils::creatorColor(Utils::Theme::VcsBase_FileModified_TextColor); return node->isEnabled() ? QVariant() : Utils::creatorColor(Utils::Theme::TextColorDisabled); case Project::FilePathRole: @@ -447,12 +449,39 @@ void FlatModel::handleProjectAdded(Project *project) parsingStateChanged(project); emit ProjectTree::instance()->nodeActionsChanged(); }); + + const FilePath &rootPath = project->rootProjectDirectory(); + IVersionControl *vc = VcsManager::findVersionControlForDirectory(rootPath); + if (!vc) + return; + vc->monitorDirectory(rootPath); + connect(vc, &IVersionControl::updateFileStatus, this, &FlatModel::updateVCStatusFor); + addOrRebuildProjectModel(project); } +void FlatModel::updateVCStatusFor(const Utils::FilePath root, const QStringList &files) +{ + std::for_each(std::begin(files), std::end(files), [root, this](const QString &file) { + const Node *node = ProjectTree::nodeForFile(root.pathAppended(file)); + + if (!node) + return; + + const QModelIndex index = indexForNode(node); + emit dataChanged(index, index, {Qt::ForegroundRole}); + }); +} + void FlatModel::handleProjectRemoved(Project *project) { destroyItem(nodeForProject(project)); + + if (!project) + return; + const FilePath &rootPath = project->rootProjectDirectory(); + if (IVersionControl *vc = VcsManager::findVersionControlForDirectory(rootPath)) + vc->stopMonitoringDirectory(rootPath); } WrapperNode *FlatModel::nodeForProject(const Project *project) const diff --git a/src/plugins/projectexplorer/projectmodels.h b/src/plugins/projectexplorer/projectmodels.h index 4aea187e107..a07279388a4 100644 --- a/src/plugins/projectexplorer/projectmodels.h +++ b/src/plugins/projectexplorer/projectmodels.h @@ -85,6 +85,7 @@ private: void rebuildModel(); void addFolderNode(WrapperNode *parent, FolderNode *folderNode, QSet *seen); bool trimEmptyDirectories(WrapperNode *parent); + void updateVCStatusFor(const Utils::FilePath root, const QStringList &files); ExpandData expandDataForNode(const Node *node) const; void loadExpandData(); diff --git a/src/plugins/projectexplorer/projectnodes.cpp b/src/plugins/projectexplorer/projectnodes.cpp index 9e4b0409a2f..99538de5aed 100644 --- a/src/plugins/projectexplorer/projectnodes.cpp +++ b/src/plugins/projectexplorer/projectnodes.cpp @@ -261,6 +261,22 @@ void FileNode::setHasError(bool error) const m_hasError = error; } +bool FileNode::hasModification() const +{ + static const QSet forbidden{FileType::Unknown, FileType::App, FileType::Lib, + FileType::FileTypeSize}; + + if (forbidden.contains(fileType())) + return false; + + const FilePath file = filePath(); + const FilePath dir = file.absolutePath(); + if (Core::IVersionControl *vc = Core::VcsManager::findVersionControlForDirectory(dir)) + return vc->hasModification(file); + + return false; +} + bool FileNode::useUnavailableMarker() const { return m_useUnavailableMarker; diff --git a/src/plugins/projectexplorer/projectnodes.h b/src/plugins/projectexplorer/projectnodes.h index bd1cb74fcd5..2a0c02af9b9 100644 --- a/src/plugins/projectexplorer/projectnodes.h +++ b/src/plugins/projectexplorer/projectnodes.h @@ -201,6 +201,8 @@ public: void setHasError(const bool error); void setHasError(const bool error) const; + bool hasModification() const; + QIcon icon() const; void setIcon(const QIcon icon);