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