Pop up an error box if history is chosen on directory
[perforce-plugin:perforce-plugin.git] / perforceplugin.cpp
1 /***************************************************************************
2  *   Copyright 2010  Morten Danielsen Volden                               *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  ***************************************************************************/
10
11 #include "perforceplugin.h"
12
13 #include <iostream>
14
15 #include <KPluginFactory>
16 #include <KPluginLoader>
17 #include <KLocalizedString>
18 #include <KAboutData>
19 #include <KActionCollection>
20 #include <QFileInfo>
21 #include <QDateTime>
22 #include <QDir>
23 #include <QProcessEnvironment>
24 #include <QMenu>
25 #include <kmessagebox.h>
26 #include <vcs/vcsjob.h>
27 #include <vcs/vcsrevision.h>
28 #include <vcs/vcsevent.h>
29 #include <vcs/dvcs/dvcsjob.h>
30 #include <vcs/vcsannotation.h>
31 #include <vcs/widgets/standardvcslocationwidget.h>
32
33 #include <interfaces/context.h>
34 #include <interfaces/contextmenuextension.h>
35 #include <interfaces/icore.h>
36 #include <interfaces/iruncontroller.h>
37
38
39
40 #include <vcs/vcspluginhelper.h>
41
42 using namespace KDevelop;
43
44 namespace
45 {
46   const QString ACTION_STR("... action ");
47   const QString CLIENT_FILE_STR("... clientFile ");
48   const QString DEPOT_FILE_STR("... depotFile ");
49   const QString LOGENTRY_START("... #");
50 }
51
52 /* Todo:
53  *
54  *                      Implement History  */
55
56 K_PLUGIN_FACTORY (KdevPerforceFactory, registerPlugin<perforceplugin>();)
57 K_EXPORT_PLUGIN (KdevPerforceFactory (KAboutData ("kdevperforce","kdevperforce", ki18n ("Support for Perforce Version Control System"), "0.1", ki18n ("Support for Perforce Version Control System"), KAboutData::License_GPL)))
58
59 perforceplugin::perforceplugin(QObject* parent, const QVariantList& ):
60         KDevelop::IPlugin(KdevPerforceFactory::componentData(), parent )
61         , m_common(new KDevelop::VcsPluginHelper(this, this))
62         , m_perforcemenu( 0 )
63         , m_perforceConfigName("p4config.txt")
64         , m_edit_action( 0 )
65 {
66     KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBasicVersionControl )
67     KDEV_USE_EXTENSION_INTERFACE( KDevelop::ICentralizedVersionControl )
68
69     QProcessEnvironment currentEviron(QProcessEnvironment::systemEnvironment());
70     // We will default search for p4config.txt - However if something else is used, search for that
71     QString tmp(currentEviron.value("P4CONFIG"));
72     if (!tmp.isEmpty())
73     {
74         m_perforceConfigName = tmp;
75     }
76 }
77
78 perforceplugin::~perforceplugin()
79 {
80 }
81
82 QString perforceplugin::name() const
83 {
84     return i18n("Perforce");
85 }
86
87 KDevelop::VcsImportMetadataWidget* perforceplugin::createImportMetadataWidget(QWidget* /*parent*/)
88 {
89     return 0;
90 }
91
92 bool perforceplugin::isValidDirectory(const KUrl & dirPath)
93 {
94     const QFileInfo finfo(dirPath.toLocalFile());
95     QDir dir = finfo.isDir() ? QDir(dirPath.toLocalFile()) : finfo.absoluteDir();
96
97     do
98     {
99         if (dir.exists(m_perforceConfigName))
100         {
101             return true;
102         }
103     }
104     while (dir.cdUp());
105     return false;
106 }
107
108 bool perforceplugin::isVersionControlled(const KUrl& localLocation)
109 {
110     QFileInfo fsObject(localLocation.toLocalFile());
111     if (fsObject.isDir())
112     {
113         return isValidDirectory(localLocation);
114     }
115     return parseP4fstat(fsObject, KDevelop::OutputJob::Silent);
116 }
117
118 DVcsJob* perforceplugin::p4fstatJob(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity)
119 {
120     DVcsJob* job = new DVcsJob(curFile.absolutePath(), this, verbosity);
121     setEnvironmentForJob(job, curFile);
122     *job << "p4" << "fstat" << curFile.fileName();
123     return job;
124 }
125
126 bool perforceplugin::parseP4fstat(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity)
127 {
128     QScopedPointer<DVcsJob> job(p4fstatJob(curFile, verbosity));
129     if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded)
130     {
131         kDebug() << "Perforce returned: " << job->output();
132         if (!job->output().isEmpty())
133             return true;
134     }
135     return false;
136 }
137
138 QString perforceplugin::getRepositoryName(const QFileInfo& curFile)
139 {
140     QString ret;
141     QScopedPointer<DVcsJob> job(p4fstatJob(curFile, KDevelop::OutputJob::Silent));
142     if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded)
143     {
144         if (!job->output().isEmpty())
145         {
146             QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts);
147             foreach(const QString& line, outputLines)
148             {
149                 int idx(line.indexOf(DEPOT_FILE_STR));
150                 if (idx != -1)
151                 {
152                     ret = line.right(line.size() - DEPOT_FILE_STR.size());
153                     return ret;
154                 }
155             }
156         }
157     }
158     
159     return ret; 
160 }
161
162 KDevelop::VcsJob* perforceplugin::repositoryLocation(const KUrl& /*localLocation*/)
163 {
164     return 0;
165 }
166
167 KDevelop::VcsJob* perforceplugin::add(const KUrl::List& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
168 {
169     QFileInfo curFile(localLocations.front().toLocalFile());
170     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
171     setEnvironmentForJob(job, curFile);
172     *job << "p4" << "add" << localLocations;
173
174     return job;
175 }
176
177 KDevelop::VcsJob* perforceplugin::remove(const KUrl::List& /*localLocations*/)
178 {
179     return 0;
180 }
181
182 KDevelop::VcsJob* perforceplugin::copy(const KUrl& /*localLocationSrc*/, const KUrl& /*localLocationDstn*/)
183 {
184     return 0;
185 }
186
187 KDevelop::VcsJob* perforceplugin::move(const KUrl& /*localLocationSrc*/, const KUrl& /*localLocationDst*/)
188 {
189     return 0;
190 }
191
192 KDevelop::VcsJob* perforceplugin::status(const KUrl::List& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
193 {
194     if (localLocations.count() != 1)
195     {
196         KMessageBox::error(0, i18n("Please select only one item for this operation"));
197         return 0;
198     }
199
200     QFileInfo curFile(localLocations.front().toLocalFile());
201
202     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
203     setEnvironmentForJob(job, curFile);
204     *job << "p4" << "fstat" << curFile.fileName();
205     connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseP4StatusOutput(KDevelop::DVcsJob*)));
206
207     return job;
208 }
209
210 KDevelop::VcsJob* perforceplugin::revert(const KUrl::List& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
211 {
212     if (localLocations.count() != 1)
213     {
214         KMessageBox::error(0, i18n("Please select only one item for this operation"));
215         return 0;
216     }
217
218     QFileInfo curFile(localLocations.front().toLocalFile());
219
220     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
221     setEnvironmentForJob(job, curFile);
222     *job << "p4" << "revert" << curFile.fileName();
223
224     return job;
225
226 }
227
228 KDevelop::VcsJob* perforceplugin::update(const KUrl::List& localLocations, const KDevelop::VcsRevision& /*rev*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
229 {
230     QFileInfo curFile(localLocations.front().toLocalFile());
231
232     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
233     setEnvironmentForJob(job, curFile);
234     *job << "p4" << "-p" << "127.0.0.1:1666" << "info";
235
236     return job;
237 }
238
239 KDevelop::VcsJob* perforceplugin::commit(const QString& message, const KUrl::List& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
240 {
241     if (localLocations.empty() || message.isEmpty())
242         return errorsFound(i18n("No files or message specified"));
243
244
245     QFileInfo curFile(localLocations.front().toLocalFile());
246
247     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
248     setEnvironmentForJob(job, curFile);
249     *job << "p4" << "submit" << "-d" << message << localLocations;
250
251     return job;
252 }
253
254 KDevelop::VcsJob* perforceplugin::diff(const KUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type , KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
255 {
256     QFileInfo curFile(fileOrDirectory.toLocalFile());
257     QString depotSrcFileName = getRepositoryName(curFile);
258     QString depotDstFileName = depotSrcFileName;
259     switch(srcRevision.revisionType())
260     {
261         case VcsRevision::Special:
262             switch(srcRevision.revisionValue().value<VcsRevision::RevisionSpecialType>()) 
263             {
264                 case VcsRevision::Head:
265                     depotSrcFileName.append("#head");
266                     break;
267                 case VcsRevision::Base:
268                     depotSrcFileName.append("#have");
269                     break;
270                 case VcsRevision::Previous:
271                     {
272                         bool *ok(new bool());
273                         int previous = dstRevision.prettyValue().toInt(ok);
274                         previous--;
275                         QString tmp;
276                         tmp.setNum(previous);
277                         depotSrcFileName.append("#");
278                         depotSrcFileName.append(tmp);
279                     }
280                     break;
281                 case VcsRevision::Working:
282                 case VcsRevision::Start:
283                 case VcsRevision::UserSpecialType:
284                     break;
285             }
286             break;
287         case VcsRevision::FileNumber:
288         case VcsRevision::GlobalNumber:
289             depotSrcFileName.append("#");
290             depotSrcFileName.append(srcRevision.prettyValue());
291             break;
292         case VcsRevision::Date:
293         case VcsRevision::Invalid:
294         case VcsRevision::UserSpecialType:
295             break;
296             
297     }
298
299     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
300     setEnvironmentForJob(job, curFile);
301     switch(dstRevision.revisionType())
302     {
303         case VcsRevision::FileNumber:
304         case VcsRevision::GlobalNumber:
305             depotDstFileName.append("#");
306             depotDstFileName.append(dstRevision.prettyValue());
307             *job << "p4" << "diff2" << "-u" << depotSrcFileName << depotDstFileName;
308             break;
309         case VcsRevision::Special:
310             switch(dstRevision.revisionValue().value<VcsRevision::RevisionSpecialType>()) 
311             {
312                 case VcsRevision::Working:
313                     *job << "p4" << "diff" << "-du" << depotSrcFileName;
314                     break;
315                 case VcsRevision::Start:
316                 case VcsRevision::UserSpecialType:
317                 default:
318                     break;
319             }
320         default:
321             break;
322     }
323     kDebug() << "########### srcRevision Is: " << srcRevision.prettyValue();
324     kDebug() << "########### dstRevision Is: " << dstRevision.prettyValue();
325
326
327     connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseP4DiffOutput(KDevelop::DVcsJob*)));
328     return job;
329 }
330
331 KDevelop::VcsJob* perforceplugin::log(const KUrl& localLocation, const KDevelop::VcsRevision& /*rev*/, long unsigned int /*limit*/)
332 {
333     QFileInfo curFile(localLocation.toLocalFile());
334     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
335     setEnvironmentForJob(job, curFile);
336     *job << "p4" << "filelog" << "-l" << localLocation;
337
338     connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseP4LogOutput(KDevelop::DVcsJob*)));
339     return job;
340 }
341
342 KDevelop::VcsJob* perforceplugin::log(const KUrl& localLocation, const KDevelop::VcsRevision& /*rev*/, const KDevelop::VcsRevision& /*limit*/)
343 {
344     QFileInfo curFile(localLocation.toLocalFile());
345     if (curFile.isDir())
346     {
347         KMessageBox::error(0, i18n("Please select a file for this operation"));
348         return errorsFound(i18n("Directory not supported for this operation"));
349     }
350     
351     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
352     setEnvironmentForJob(job, curFile);
353     *job << "p4" << "filelog" << "-lt" << localLocation;
354     
355     connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseP4LogOutput(KDevelop::DVcsJob*)));
356     return job;
357 }
358
359 KDevelop::VcsJob* perforceplugin::annotate(const KUrl& /*localLocation*/, const KDevelop::VcsRevision& /*rev*/)
360 {
361     return 0;
362 }
363
364 KDevelop::VcsJob* perforceplugin::resolve(const KUrl::List& /*localLocations*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
365 {
366     return 0;
367 }
368
369 KDevelop::VcsJob* perforceplugin::createWorkingCopy(const KDevelop::VcsLocation& /*sourceRepository*/, const KUrl& /*destinationDirectory*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/)
370 {
371     return 0;
372 }
373
374 KDevelop::VcsLocationWidget* perforceplugin::vcsLocation(QWidget* parent) const
375 {
376     return new StandardVcsLocationWidget(parent);
377 }
378
379
380 KDevelop::VcsJob* perforceplugin::edit(const KUrl& localLocation)
381 {
382     QFileInfo curFile(localLocation.toLocalFile());
383
384     DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose);
385     setEnvironmentForJob(job, curFile);
386     *job << "p4" << "edit" << curFile.fileName();
387
388     return job;
389 }
390
391 KDevelop::VcsJob* perforceplugin::unedit(const KUrl& /*localLocation*/)
392 {
393     return 0;
394 }
395
396 KDevelop::VcsJob* perforceplugin::localRevision(const KUrl& /*localLocation*/, KDevelop::VcsRevision::RevisionType )
397 {
398     return 0;
399 }
400
401 KDevelop::VcsJob* perforceplugin::import(const QString& /*commitMessage*/, const KUrl& /*sourceDirectory*/, const KDevelop::VcsLocation& /*destinationRepository*/)
402 {
403     return 0;
404 }
405
406 KDevelop::ContextMenuExtension perforceplugin::contextMenuExtension(KDevelop::Context* context)
407 {
408     m_common->setupFromContext(context);
409
410     const KUrl::List & ctxUrlList  = m_common->contextUrlList();
411
412     bool hasVersionControlledEntries = false;
413     foreach(KUrl const& url, ctxUrlList)
414     {
415         if (isValidDirectory(url))
416         {
417             hasVersionControlledEntries = true;
418             break;
419         }
420     }
421
422     //kDebug() << "version controlled?" << hasVersionControlledEntries;
423
424     if (!hasVersionControlledEntries)
425         return IPlugin::contextMenuExtension(context);
426
427     QMenu * perforceMenu = m_common->commonActions();
428     perforceMenu->addSeparator();
429
430     perforceMenu->addSeparator();
431
432     if ( !m_edit_action )
433     {
434         m_edit_action = new KAction(i18n("Edit"), this);
435         connect(m_edit_action, SIGNAL(triggered()), this, SLOT(ctxEdit()));
436     }
437     perforceMenu->addAction(m_edit_action);
438
439     ContextMenuExtension menuExt;
440     menuExt.addAction(ContextMenuExtension::VcsGroup, perforceMenu->menuAction());
441
442     return menuExt;
443 }
444
445 void perforceplugin::ctxEdit()
446 {
447     KUrl::List const & ctxUrlList = m_common->contextUrlList();
448     if (ctxUrlList.count() != 1)
449     {
450         KMessageBox::error(0, i18n("Please select only one item for this operation"));
451         return;
452     }
453     KUrl source = ctxUrlList.first();
454     KDevelop::ICore::self()->runController()->registerJob(edit(source));
455 }
456
457 void perforceplugin::setEnvironmentForJob(DVcsJob* job, const QFileInfo& curFile)
458 {
459     KProcess* jobproc = job->process();
460     QStringList beforeEnv = jobproc->environment();
461     //kDebug() << "Before setting the environment : " << beforeEnv;
462     jobproc->setEnv("P4CONFIG", m_perforceConfigName);
463     jobproc->setEnv("PWD", curFile.absolutePath());
464     QStringList afterEnv = jobproc->environment();
465     //kDebug() << "After setting the environment : " << afterEnv;
466 }
467
468 void perforceplugin::parseP4StatusOutput(DVcsJob* job)
469 {
470     QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts);
471     //KUrl fileUrl = job->directory().absolutePath();
472     QVariantList statuses;
473     QList<KUrl> processedFiles;
474
475
476     VcsStatusInfo status;
477     status.setState(VcsStatusInfo::ItemUserState);
478     foreach(const QString& line, outputLines)
479     {
480         int idx(line.indexOf(ACTION_STR));
481         if (idx != -1)
482         {
483             QString curr = line.right(line.size() - ACTION_STR.size());
484             kDebug() << "PARSED FROM P4 FSTAT JOB " << curr;
485
486             if (curr == "edit")
487             {
488                 status.setState(VcsStatusInfo::ItemModified);
489             }
490             else if (curr == "add")
491             {
492                 status.setState(VcsStatusInfo::ItemAdded);
493             }
494             else
495             {
496                 status.setState(VcsStatusInfo::ItemUserState);
497             }
498             continue;
499         }
500         idx = line.indexOf(CLIENT_FILE_STR);
501         if (idx != -1)
502         {
503             KUrl fileUrl = line.right(line.size() - CLIENT_FILE_STR.size());
504             kDebug() << "PARSED URL FROM P4 FSTAT JOB " << fileUrl.url();
505             status.setUrl(fileUrl);
506         }       
507     }
508     statuses.append(qVariantFromValue<VcsStatusInfo>(status));
509     job->setResults(statuses);
510 }
511
512 void perforceplugin::parseP4LogOutput(KDevelop::DVcsJob* job)
513 {
514     QList<QVariant> commits;
515     VcsEvent item;
516     QString commitMessage;
517     QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts);
518     bool foundAChangelist(false);
519     /// I'm pretty sure this could be done more elegant.
520     foreach(const QString& line, outputLines)
521     {
522         int idx(line.indexOf(LOGENTRY_START));
523         if (idx != -1)
524         {
525             if(!foundAChangelist)
526             {
527                 foundAChangelist = true;
528             }
529             else
530             {
531                 item.setMessage(commitMessage.trimmed()); 
532                 commits.append(QVariant::fromValue(item));
533                 commitMessage.clear();
534             }
535             // expecting the Logentry line to be of the form:
536             //... #5 change 10 edit on 2010/12/06 12:07:31 by mvo@testbed (text)
537             //QString changeNumber(line.section(' ', 3, 3 ));
538             QString localChangeNumber(line.section(' ', 1, 1 ));
539             localChangeNumber.remove(0, 1); // Remove the # from the local revision number
540             
541             QString author(line.section(' ', 9, 9 ));
542             VcsRevision rev;
543             rev.setRevisionValue(localChangeNumber, KDevelop::VcsRevision::FileNumber);
544             item.setRevision(rev);
545             item.setAuthor(author);
546             item.setDate(QDateTime::fromString(line.section(' ', 6, 7 ), "yyyy/MM/dd hh:mm:ss") );
547         } 
548         else
549         {
550             if(foundAChangelist)
551                 commitMessage += line +'\n';
552         }
553             
554     }
555     item.setMessage(commitMessage); 
556     commits.append(QVariant::fromValue(item));
557     
558     job->setResults(commits);
559 }
560
561
562 void perforceplugin::parseP4DiffOutput(DVcsJob* job)
563 {
564     VcsDiff diff;
565     diff.setDiff(job->output());
566
567     QDir dir(job->directory());
568
569     do
570     {
571         if (dir.exists(m_perforceConfigName))
572         {
573             break;
574         }
575     }
576     while (dir.cdUp());
577
578     diff.setBaseDiff(KUrl(dir.absolutePath()));
579
580     job->setResults(qVariantFromValue(diff));
581 }
582
583 KDevelop::VcsJob* perforceplugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity)
584 {
585     DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity);
586     *j << "echo" << i18n("error: %1", error) << "-n";
587     return j;
588 }
589
590
591