1 /*
2     SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "projectchangesmodel.h"
8 
9 #include "debug.h"
10 
11 #include <KLocalizedString>
12 
13 #include <vcs/interfaces/ibasicversioncontrol.h>
14 #include <interfaces/ibranchingversioncontrol.h>
15 #include <interfaces/iplugin.h>
16 #include <interfaces/iproject.h>
17 #include <interfaces/icore.h>
18 #include <interfaces/iplugincontroller.h>
19 #include <interfaces/iprojectcontroller.h>
20 #include <vcs/vcsstatusinfo.h>
21 #include <vcs/vcsjob.h>
22 #include <interfaces/iruncontroller.h>
23 #include <interfaces/idocumentcontroller.h>
24 #include <project/projectmodel.h>
25 #include <util/path.h>
26 
27 #include <QDir>
28 #include <QIcon>
29 
30 #include <array>
31 
32 using namespace KDevelop;
33 
ProjectChangesModel(QObject * parent)34 ProjectChangesModel::ProjectChangesModel(QObject* parent)
35     : VcsFileChangesModel(parent)
36 {
37     const auto projects = ICore::self()->projectController()->projects();
38     for (IProject* p : projects) {
39         addProject(p);
40     }
41 
42     connect(ICore::self()->projectController(), &IProjectController::projectOpened,
43                                               this, &ProjectChangesModel::addProject);
44     connect(ICore::self()->projectController(), &IProjectController::projectClosing,
45                                               this, &ProjectChangesModel::removeProject);
46 
47     connect(ICore::self()->documentController(), &IDocumentController::documentSaved,
48                                                 this, &ProjectChangesModel::documentSaved);
49     connect(ICore::self()->projectController()->projectModel(), &ProjectModel::rowsInserted,
50                                                 this, &ProjectChangesModel::itemsAdded);
51 
52     connect(ICore::self()->runController(), &IRunController::jobUnregistered, this, &ProjectChangesModel::jobUnregistered);
53 }
54 
~ProjectChangesModel()55 ProjectChangesModel::~ProjectChangesModel()
56 {}
57 
addProject(IProject * p)58 void ProjectChangesModel::addProject(IProject* p)
59 {
60     auto* it = new QStandardItem(p->name());
61     it->setData(p->name(), ProjectChangesModel::ProjectNameRole);
62     IPlugin* plugin = p->versionControlPlugin();
63     if(plugin) {
64         auto* vcs = plugin->extension<IBasicVersionControl>();
65 
66         auto info = ICore::self()->pluginController()->pluginInfo(plugin);
67 
68         it->setIcon(QIcon::fromTheme(info.iconName()));
69         it->setToolTip(vcs->name());
70 
71         auto* branchingExtension = plugin->extension<KDevelop::IBranchingVersionControl>();
72         if(branchingExtension) {
73             const auto pathUrl = p->path().toUrl();
74             branchingExtension->registerRepositoryForCurrentBranchChanges(pathUrl);
75             // can't use new signal slot syntax here, IBranchingVersionControl is not a QObject
76             connect(plugin, SIGNAL(repositoryBranchChanged(QUrl)), this, SLOT(repositoryBranchChanged(QUrl)));
77             repositoryBranchChanged(pathUrl);
78         } else
79             reload(QList<IProject*>() << p);
80     } else {
81         it->setEnabled(false);
82     }
83 
84     appendRow(it);
85 }
86 
removeProject(IProject * p)87 void ProjectChangesModel::removeProject(IProject* p)
88 {
89     QStandardItem* it=projectItem(p);
90     if (!it) {
91         // when the project is closed before it was fully populated, we won't ever see a
92         // projectOpened signal - handle this gracefully by just ignoring the remove request
93         return;
94     }
95     removeRow(it->row());
96 }
97 
findItemChild(QStandardItem * parent,const QVariant & value,int role=Qt::DisplayRole)98 QStandardItem* findItemChild(QStandardItem* parent, const QVariant& value, int role = Qt::DisplayRole)
99 {
100     for(int i=0; i<parent->rowCount(); i++) {
101         QStandardItem* curr=parent->child(i);
102 
103         if(curr->data(role) == value)
104             return curr;
105     }
106     return nullptr;
107 }
108 
projectItem(IProject * p) const109 QStandardItem* ProjectChangesModel::projectItem(IProject* p) const
110 {
111     return findItemChild(invisibleRootItem(), p->name(), ProjectChangesModel::ProjectNameRole);
112 }
113 
updateState(IProject * p,const KDevelop::VcsStatusInfo & status)114 void ProjectChangesModel::updateState(IProject* p, const KDevelop::VcsStatusInfo& status)
115 {
116     QStandardItem* pItem = projectItem(p);
117     Q_ASSERT(pItem);
118 
119     VcsFileChangesModel::updateState(pItem, status);
120 }
121 
changes(IProject * project,const QList<QUrl> & urls,IBasicVersionControl::RecursionMode mode)122 void ProjectChangesModel::changes(IProject* project, const QList<QUrl>& urls, IBasicVersionControl::RecursionMode mode)
123 {
124     IPlugin* vcsplugin=project->versionControlPlugin();
125     IBasicVersionControl* vcs = vcsplugin ? vcsplugin->extension<IBasicVersionControl>() : nullptr;
126 
127     if(vcs && vcs->isVersionControlled(urls.first())) { //TODO: filter?
128         VcsJob* job=vcs->status(urls, mode);
129         job->setProperty("urls", QVariant::fromValue<QList<QUrl>>(urls));
130         job->setProperty("mode", QVariant::fromValue<int>(mode));
131         job->setProperty("project", QVariant::fromValue(project));
132         connect(job, &VcsJob::finished, this, &ProjectChangesModel::statusReady);
133 
134         ICore::self()->runController()->registerJob(job);
135     }
136 }
137 
statusReady(KJob * job)138 void ProjectChangesModel::statusReady(KJob* job)
139 {
140     auto* status=static_cast<VcsJob*>(job);
141 
142     const QList<QVariant> states = status->fetchResults().toList();
143     auto* project = job->property("project").value<KDevelop::IProject*>();
144     if(!project)
145         return;
146 
147     QSet<QUrl> foundUrls;
148     foundUrls.reserve(states.size());
149     for (const QVariant& state : states) {
150         const VcsStatusInfo st = state.value<VcsStatusInfo>();
151         foundUrls += st.url();
152 
153         updateState(project, st);
154     }
155 
156     QStandardItem* itProject = projectItem(project);
157     if (!itProject) {
158         qCDebug(PROJECT) << "Project no longer listed in model:" << project->name() << "- skipping update";
159         return;
160     }
161 
162     IBasicVersionControl::RecursionMode mode = IBasicVersionControl::RecursionMode(job->property("mode").toInt());
163     const QList<QUrl> projectUrls = urls(itProject);
164 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
165     const QSet<QUrl> uncertainUrls = QSet<QUrl>(projectUrls.begin(), projectUrls.end()).subtract(foundUrls);
166 #else
167     const QSet<QUrl> uncertainUrls = projectUrls.toSet().subtract(foundUrls);
168 #endif
169     const QList<QUrl> sourceUrls = job->property("urls").value<QList<QUrl>>();
170     for (const QUrl& url : sourceUrls) {
171         if(url.isLocalFile() && QDir(url.toLocalFile()).exists()) {
172             for (const QUrl& currentUrl : uncertainUrls) {
173                 if((mode == IBasicVersionControl::NonRecursive && currentUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash) == url.adjusted(QUrl::StripTrailingSlash))
174                     || (mode == IBasicVersionControl::Recursive && url.isParentOf(currentUrl))
175                 ) {
176                     removeUrl(currentUrl);
177                 }
178             }
179         }
180     }
181 }
182 
documentSaved(KDevelop::IDocument * document)183 void ProjectChangesModel::documentSaved(KDevelop::IDocument* document)
184 {
185     reload({document->url()});
186 }
187 
itemsAdded(const QModelIndex & parent,int start,int end)188 void ProjectChangesModel::itemsAdded(const QModelIndex& parent, int start, int end)
189 {
190     ProjectModel* model=ICore::self()->projectController()->projectModel();
191     ProjectBaseItem* item=model->itemFromIndex(parent);
192 
193     if(!item)
194         return;
195 
196     IProject* project=item->project();
197 
198     if(!project)
199         return;
200 
201     QList<QUrl> urls;
202 
203     for(int i=start; i<end; i++) {
204         QModelIndex idx=parent.model()->index(i, 0, parent);
205         item=model->itemFromIndex(idx);
206 
207         if(item->type()==ProjectBaseItem::File || item->type()==ProjectBaseItem::Folder || item->type()==ProjectBaseItem::BuildFolder)
208             urls += item->path().toUrl();
209     }
210 
211     if(!urls.isEmpty())
212         changes(project, urls, KDevelop::IBasicVersionControl::NonRecursive);
213 }
214 
reload(const QList<IProject * > & projects)215 void ProjectChangesModel::reload(const QList<IProject*>& projects)
216 {
217     for (IProject* project : projects) {
218         changes(project, {project->path().toUrl()}, KDevelop::IBasicVersionControl::Recursive);
219     }
220 }
221 
reload(const QList<QUrl> & urls)222 void ProjectChangesModel::reload(const QList<QUrl>& urls)
223 {
224     for (const QUrl& url : urls) {
225         IProject* project=ICore::self()->projectController()->findProjectForUrl(url);
226 
227         if (project) {
228             // FIXME: merge multiple urls of the same project
229             changes(project, {url}, KDevelop::IBasicVersionControl::NonRecursive);
230         }
231     }
232 }
233 
reloadAll()234 void ProjectChangesModel::reloadAll()
235 {
236     QList< IProject* > projects = ICore::self()->projectController()->projects();
237     reload(projects);
238 }
239 
jobUnregistered(KJob * job)240 void ProjectChangesModel::jobUnregistered(KJob* job)
241 {
242     static const std::array<VcsJob::JobType, 7> readOnly = {
243         KDevelop::VcsJob::Add,
244         KDevelop::VcsJob::Remove,
245         KDevelop::VcsJob::Pull,
246         KDevelop::VcsJob::Commit,
247         KDevelop::VcsJob::Move,
248         KDevelop::VcsJob::Copy,
249         KDevelop::VcsJob::Revert,
250     };
251 
252     auto* vcsjob = qobject_cast<VcsJob*>(job);
253     if (vcsjob && std::find(readOnly.begin(), readOnly.end(), vcsjob->type()) != readOnly.end()) {
254         reloadAll();
255     }
256 }
257 
repositoryBranchChanged(const QUrl & url)258 void ProjectChangesModel::repositoryBranchChanged(const QUrl& url)
259 {
260     IProject* project = ICore::self()->projectController()->findProjectForUrl(url);
261     if(project) {
262         IPlugin* v = project->versionControlPlugin();
263         Q_ASSERT(v);
264         auto* branching = v->extension<IBranchingVersionControl>();
265         Q_ASSERT(branching);
266         VcsJob* job = branching->currentBranch(url);
267         connect(job, &VcsJob::resultsReady, this, &ProjectChangesModel::branchNameReady);
268         job->setProperty("project", QVariant::fromValue<QObject*>(project));
269         ICore::self()->runController()->registerJob(job);
270     }
271 }
272 
branchNameReady(VcsJob * job)273 void ProjectChangesModel::branchNameReady(VcsJob* job)
274 {
275     auto* project = qobject_cast<IProject*>(job->property("project").value<QObject*>());
276     if(job->status()==VcsJob::JobSucceeded) {
277         QString name = job->fetchResults().toString();
278         const QString branchName = name.isEmpty() ? i18nc("@item:intext", "no branch") : name;
279         projectItem(project)->setText(i18nc("project name (branch name)", "%1 (%2)", project->name(), branchName));
280     } else {
281         projectItem(project)->setText(project->name());
282     }
283 
284     reload(QList<IProject*>() << project);
285 }
286