OSDN Git Service

It's 2011 now.
[qt-creator-jp/qt-creator-jp.git] / src / plugins / cvs / cvsplugin.cpp
1 /**************************************************************************
2 **
3 ** This file is part of Qt Creator
4 **
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
6 **
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
8 **
9 ** No Commercial Usage
10 **
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
14 ** this package.
15 **
16 ** GNU Lesser General Public License Usage
17 **
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file.  Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Nokia gives you certain additional
26 ** rights.  These rights are described in the Nokia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** If you have questions regarding the use of this file, please contact
30 ** Nokia at qt-info@nokia.com.
31 **
32 **************************************************************************/
33
34 #include "cvsplugin.h"
35 #include "settingspage.h"
36 #include "cvseditor.h"
37 #include "cvssubmiteditor.h"
38 #include "cvsconstants.h"
39 #include "cvscontrol.h"
40 #include "checkoutwizard.h"
41
42 #include <vcsbase/basevcseditorfactory.h>
43 #include <vcsbase/vcsbaseeditor.h>
44 #include <vcsbase/basevcssubmiteditorfactory.h>
45 #include <vcsbase/vcsbaseoutputwindow.h>
46 #include <locator/commandlocator.h>
47 #include <utils/synchronousprocess.h>
48 #include <utils/parameteraction.h>
49
50 #include <coreplugin/icore.h>
51 #include <coreplugin/coreconstants.h>
52 #include <coreplugin/filemanager.h>
53 #include <coreplugin/messagemanager.h>
54 #include <coreplugin/mimedatabase.h>
55 #include <coreplugin/actionmanager/actionmanager.h>
56 #include <coreplugin/actionmanager/actioncontainer.h>
57 #include <coreplugin/actionmanager/command.h>
58 #include <coreplugin/uniqueidmanager.h>
59 #include <coreplugin/editormanager/editormanager.h>
60 #include <coreplugin/vcsmanager.h>
61 #include <utils/stringutils.h>
62 #include <utils/qtcassert.h>
63
64 #include <QtCore/QDebug>
65 #include <QtCore/QDate>
66 #include <QtCore/QDir>
67 #include <QtCore/QFileInfo>
68 #include <QtCore/QTextCodec>
69 #include <QtCore/QtPlugin>
70 #include <QtCore/QTemporaryFile>
71 #include <QtGui/QAction>
72 #include <QtGui/QMainWindow>
73 #include <QtGui/QMenu>
74 #include <QtGui/QMessageBox>
75
76 namespace CVS {
77     namespace Internal {
78
79 static inline QString msgCannotFindTopLevel(const QString &f)
80 {
81     return CVSPlugin::tr("Cannot find repository for '%1'").
82             arg(QDir::toNativeSeparators(f));
83 }
84
85 static inline QString msgLogParsingFailed()
86 {
87     return CVSPlugin::tr("Parsing of the log output failed");
88 }
89
90 static const char * const CMD_ID_CVS_MENU    = "CVS.Menu";
91 static const char * const CMD_ID_ADD                = "CVS.Add";
92 static const char * const CMD_ID_DELETE_FILE        = "CVS.Delete";
93 static const char * const CMD_ID_EDIT_FILE          = "CVS.EditFile";
94 static const char * const CMD_ID_UNEDIT_FILE        = "CVS.UneditFile";
95 static const char * const CMD_ID_UNEDIT_REPOSITORY  = "CVS.UneditRepository";
96 static const char * const CMD_ID_REVERT             = "CVS.Revert";
97 static const char * const CMD_ID_SEPARATOR0         = "CVS.Separator0";
98 static const char * const CMD_ID_DIFF_PROJECT       = "CVS.DiffAll";
99 static const char * const CMD_ID_DIFF_CURRENT       = "CVS.DiffCurrent";
100 static const char * const CMD_ID_SEPARATOR1         = "CVS.Separator1";
101 static const char * const CMD_ID_COMMIT_ALL         = "CVS.CommitAll";
102 static const char * const CMD_ID_REVERT_ALL         = "CVS.RevertAll";
103 static const char * const CMD_ID_COMMIT_CURRENT     = "CVS.CommitCurrent";
104 static const char * const CMD_ID_SEPARATOR2         = "CVS.Separator2";
105 static const char * const CMD_ID_FILELOG_CURRENT    = "CVS.FilelogCurrent";
106 static const char * const CMD_ID_ANNOTATE_CURRENT   = "CVS.AnnotateCurrent";
107 static const char * const CMD_ID_STATUS             = "CVS.Status";
108 static const char * const CMD_ID_UPDATE             = "CVS.Update";
109 static const char * const CMD_ID_PROJECTLOG         = "CVS.ProjectLog";
110 static const char * const CMD_ID_PROJECTCOMMIT      = "CVS.ProjectCommit";
111 static const char * const CMD_ID_REPOSITORYLOG      = "CVS.RepositoryLog";
112 static const char * const CMD_ID_REPOSITORYDIFF     = "CVS.RepositoryDiff";
113 static const char * const CMD_ID_REPOSITORYSTATUS   = "CVS.RepositoryStatus";
114 static const char * const CMD_ID_REPOSITORYUPDATE   = "CVS.RepositoryUpdate";
115 static const char * const CMD_ID_SEPARATOR3         = "CVS.Separator3";
116
117 static const VCSBase::VCSBaseEditorParameters editorParameters[] = {
118 {
119     VCSBase::RegularCommandOutput,
120     "CVS Command Log Editor", // id
121     QT_TRANSLATE_NOOP("VCS", "CVS Command Log Editor"), // display name
122     "CVS Command Log Editor", // context
123     "application/vnd.nokia.text.scs_cvs_commandlog",
124     "scslog"},
125 {   VCSBase::LogOutput,
126     "CVS File Log Editor",   // id
127     QT_TRANSLATE_NOOP("VCS", "CVS File Log Editor"),   // display name
128     "CVS File Log Editor",   // context
129     "application/vnd.nokia.text.scs_cvs_filelog",
130     "scsfilelog"},
131 {    VCSBase::AnnotateOutput,
132     "CVS Annotation Editor",  // id
133     QT_TRANSLATE_NOOP("VCS", "CVS Annotation Editor"),  // display name
134     "CVS Annotation Editor",  // context
135     "application/vnd.nokia.text.scs_cvs_annotation",
136     "scsannotate"},
137 {   VCSBase::DiffOutput,
138     "CVS Diff Editor",  // id
139     QT_TRANSLATE_NOOP("VCS", "CVS Diff Editor"),  // display name
140     "CVS Diff Editor",  // context
141     "text/x-patch","diff"}
142 };
143
144 // Utility to find a parameter set by type
145 static inline const VCSBase::VCSBaseEditorParameters *findType(int ie)
146 {
147     const VCSBase::EditorContentType et = static_cast<VCSBase::EditorContentType>(ie);
148     return  VCSBase::VCSBaseEditor::findType(editorParameters, sizeof(editorParameters)/sizeof(VCSBase::VCSBaseEditorParameters), et);
149 }
150
151 static inline QString debugCodec(const QTextCodec *c)
152 {
153     return c ? QString::fromAscii(c->name()) : QString::fromAscii("Null codec");
154 }
155
156 Core::IEditor* locateEditor(const char *property, const QString &entry)
157 {
158     foreach (Core::IEditor *ed, Core::EditorManager::instance()->openedEditors())
159         if (ed->property(property).toString() == entry)
160             return ed;
161     return 0;
162 }
163
164 static inline bool messageBoxQuestion(const QString &title, const QString &question, QWidget *parent = 0)
165 {
166     return QMessageBox::question(parent, title, question, QMessageBox::Yes|QMessageBox::No) == QMessageBox::Yes;
167 }
168
169 // ------------- CVSPlugin
170 CVSPlugin *CVSPlugin::m_cvsPluginInstance = 0;
171
172 CVSPlugin::CVSPlugin() :
173     VCSBase::VCSBasePlugin(QLatin1String(CVS::Constants::CVSCOMMITEDITOR_ID)),
174     m_commandLocator(0),
175     m_addAction(0),
176     m_deleteAction(0),
177     m_revertAction(0),
178     m_editCurrentAction(0),
179     m_uneditCurrentAction(0),
180     m_uneditRepositoryAction(0),
181     m_diffProjectAction(0),
182     m_diffCurrentAction(0),
183     m_logProjectAction(0),
184     m_logRepositoryAction(0),
185     m_commitAllAction(0),
186     m_revertRepositoryAction(0),
187     m_commitCurrentAction(0),
188     m_filelogCurrentAction(0),
189     m_annotateCurrentAction(0),
190     m_statusProjectAction(0),
191     m_updateProjectAction(0),
192     m_commitProjectAction(0),
193     m_diffRepositoryAction(0),
194     m_updateRepositoryAction(0),
195     m_statusRepositoryAction(0),
196     m_submitCurrentLogAction(0),
197     m_submitDiffAction(0),
198     m_submitUndoAction(0),
199     m_submitRedoAction(0),
200     m_menuAction(0),
201     m_submitActionTriggered(false)
202 {
203 }
204
205 CVSPlugin::~CVSPlugin()
206 {
207     cleanCommitMessageFile();
208 }
209
210 void CVSPlugin::cleanCommitMessageFile()
211 {
212     if (!m_commitMessageFileName.isEmpty()) {
213         QFile::remove(m_commitMessageFileName);
214         m_commitMessageFileName.clear();
215         m_commitRepository.clear();
216     }
217 }
218 bool CVSPlugin::isCommitEditorOpen() const
219 {
220     return !m_commitMessageFileName.isEmpty();
221 }
222
223 static const VCSBase::VCSBaseSubmitEditorParameters submitParameters = {
224     CVS::Constants::CVS_SUBMIT_MIMETYPE,
225     CVS::Constants::CVSCOMMITEDITOR_ID,
226     CVS::Constants::CVSCOMMITEDITOR_DISPLAY_NAME,
227     CVS::Constants::CVSCOMMITEDITOR
228 };
229
230 static inline Core::Command *createSeparator(QObject *parent,
231                                              Core::ActionManager *ami,
232                                              const char *id,
233                                              const Core::Context &globalcontext)
234 {
235     QAction *tmpaction = new QAction(parent);
236     tmpaction->setSeparator(true);
237     return ami->registerAction(tmpaction, id, globalcontext);
238 }
239
240 bool CVSPlugin::initialize(const QStringList & /*arguments */, QString *errorMessage)
241 {
242     typedef VCSBase::VCSSubmitEditorFactory<CVSSubmitEditor> CVSSubmitEditorFactory;
243     typedef VCSBase::VCSEditorFactory<CVSEditor> CVSEditorFactory;
244     using namespace Constants;
245
246     using namespace Core::Constants;
247     using namespace ExtensionSystem;
248
249     VCSBase::VCSBasePlugin::initialize(new CVSControl(this));
250
251     m_cvsPluginInstance = this;
252     Core::ICore *core = Core::ICore::instance();
253
254     if (!core->mimeDatabase()->addMimeTypes(QLatin1String(":/trolltech.cvs/CVS.mimetypes.xml"), errorMessage))
255         return false;
256
257     if (QSettings *settings = core->settings())
258         m_settings.fromSettings(settings);
259
260     addAutoReleasedObject(new SettingsPage);
261
262     addAutoReleasedObject(new CVSSubmitEditorFactory(&submitParameters));
263
264     static const char *describeSlotC = SLOT(slotDescribe(QString,QString));
265     const int editorCount = sizeof(editorParameters)/sizeof(VCSBase::VCSBaseEditorParameters);
266     for (int i = 0; i < editorCount; i++)
267         addAutoReleasedObject(new CVSEditorFactory(editorParameters + i, this, describeSlotC));
268
269     addAutoReleasedObject(new CheckoutWizard);
270
271     const QString prefix = QLatin1String("cvs");
272     m_commandLocator = new Locator::CommandLocator(QLatin1String("CVS"), prefix, prefix);
273     addAutoReleasedObject(m_commandLocator);
274
275     //register actions
276     Core::ActionManager *ami = core->actionManager();
277     Core::ActionContainer *toolsContainer = ami->actionContainer(M_TOOLS);
278
279     Core::ActionContainer *cvsMenu = ami->createMenu(Core::Id(CMD_ID_CVS_MENU));
280     cvsMenu->menu()->setTitle(tr("&CVS"));
281     toolsContainer->addMenu(cvsMenu);
282     m_menuAction = cvsMenu->menu()->menuAction();
283
284     Core::Context globalcontext(C_GLOBAL);
285
286     Core::Command *command;
287
288     m_diffCurrentAction = new Utils::ParameterAction(tr("Diff Current File"), tr("Diff \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
289     command = ami->registerAction(m_diffCurrentAction,
290         CMD_ID_DIFF_CURRENT, globalcontext);
291     command->setAttribute(Core::Command::CA_UpdateText);
292     command->setDefaultKeySequence(QKeySequence(tr("Alt+C,Alt+D")));
293     connect(m_diffCurrentAction, SIGNAL(triggered()), this, SLOT(diffCurrentFile()));
294     cvsMenu->addAction(command);
295     m_commandLocator->appendCommand(command);
296
297     m_filelogCurrentAction = new Utils::ParameterAction(tr("Filelog Current File"), tr("Filelog \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
298     command = ami->registerAction(m_filelogCurrentAction,
299         CMD_ID_FILELOG_CURRENT, globalcontext);
300     command->setAttribute(Core::Command::CA_UpdateText);
301     connect(m_filelogCurrentAction, SIGNAL(triggered()), this,
302         SLOT(filelogCurrentFile()));
303     cvsMenu->addAction(command);
304     m_commandLocator->appendCommand(command);
305
306     m_annotateCurrentAction = new Utils::ParameterAction(tr("Annotate Current File"), tr("Annotate \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
307     command = ami->registerAction(m_annotateCurrentAction,
308         CMD_ID_ANNOTATE_CURRENT, globalcontext);
309     command->setAttribute(Core::Command::CA_UpdateText);
310     connect(m_annotateCurrentAction, SIGNAL(triggered()), this,
311         SLOT(annotateCurrentFile()));
312     cvsMenu->addAction(command);
313     m_commandLocator->appendCommand(command);
314
315     cvsMenu->addAction(createSeparator(this, ami, CMD_ID_SEPARATOR0, globalcontext));
316
317     m_addAction = new Utils::ParameterAction(tr("Add"), tr("Add \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
318     command = ami->registerAction(m_addAction, CMD_ID_ADD,
319         globalcontext);
320     command->setAttribute(Core::Command::CA_UpdateText);
321     command->setDefaultKeySequence(QKeySequence(tr("Alt+C,Alt+A")));
322     connect(m_addAction, SIGNAL(triggered()), this, SLOT(addCurrentFile()));
323     cvsMenu->addAction(command);
324     m_commandLocator->appendCommand(command);
325
326     m_commitCurrentAction = new Utils::ParameterAction(tr("Commit Current File"), tr("Commit \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
327     command = ami->registerAction(m_commitCurrentAction,
328         CMD_ID_COMMIT_CURRENT, globalcontext);
329     command->setAttribute(Core::Command::CA_UpdateText);
330     command->setDefaultKeySequence(QKeySequence(tr("Alt+C,Alt+C")));
331     connect(m_commitCurrentAction, SIGNAL(triggered()), this, SLOT(startCommitCurrentFile()));
332     cvsMenu->addAction(command);
333     m_commandLocator->appendCommand(command);
334
335     m_deleteAction = new Utils::ParameterAction(tr("Delete..."), tr("Delete \"%1\"..."), Utils::ParameterAction::EnabledWithParameter, this);
336     command = ami->registerAction(m_deleteAction, CMD_ID_DELETE_FILE,
337         globalcontext);
338     command->setAttribute(Core::Command::CA_UpdateText);
339     connect(m_deleteAction, SIGNAL(triggered()), this, SLOT(promptToDeleteCurrentFile()));
340     cvsMenu->addAction(command);
341     m_commandLocator->appendCommand(command);
342
343     m_revertAction = new Utils::ParameterAction(tr("Revert..."), tr("Revert \"%1\"..."), Utils::ParameterAction::EnabledWithParameter, this);
344     command = ami->registerAction(m_revertAction, CMD_ID_REVERT,
345         globalcontext);
346     command->setAttribute(Core::Command::CA_UpdateText);
347     connect(m_revertAction, SIGNAL(triggered()), this, SLOT(revertCurrentFile()));
348     cvsMenu->addAction(command);
349     m_commandLocator->appendCommand(command);
350
351     cvsMenu->addAction(createSeparator(this, ami, CMD_ID_SEPARATOR1, globalcontext));
352
353     m_editCurrentAction = new Utils::ParameterAction(tr("Edit"), tr("Edit \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
354     command = ami->registerAction(m_editCurrentAction, CMD_ID_EDIT_FILE, globalcontext);
355     command->setAttribute(Core::Command::CA_UpdateText);
356     connect(m_editCurrentAction, SIGNAL(triggered()), this, SLOT(editCurrentFile()));
357     cvsMenu->addAction(command);
358     m_commandLocator->appendCommand(command);
359
360     m_uneditCurrentAction = new Utils::ParameterAction(tr("Unedit"), tr("Unedit \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
361     command = ami->registerAction(m_uneditCurrentAction, CMD_ID_UNEDIT_FILE, globalcontext);
362     command->setAttribute(Core::Command::CA_UpdateText);
363     connect(m_uneditCurrentAction, SIGNAL(triggered()), this, SLOT(uneditCurrentFile()));
364     cvsMenu->addAction(command);
365     m_commandLocator->appendCommand(command);
366
367     m_uneditRepositoryAction = new QAction(tr("Unedit Repository"), this);
368     command = ami->registerAction(m_uneditRepositoryAction, CMD_ID_UNEDIT_REPOSITORY, globalcontext);
369     connect(m_uneditRepositoryAction, SIGNAL(triggered()), this, SLOT(uneditCurrentRepository()));
370     cvsMenu->addAction(command);
371     m_commandLocator->appendCommand(command);
372
373     cvsMenu->addAction(createSeparator(this, ami, CMD_ID_SEPARATOR2, globalcontext));
374
375     m_diffProjectAction = new Utils::ParameterAction(tr("Diff Project"), tr("Diff Project \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
376     command = ami->registerAction(m_diffProjectAction, CMD_ID_DIFF_PROJECT,
377         globalcontext);
378     command->setAttribute(Core::Command::CA_UpdateText);
379     connect(m_diffProjectAction, SIGNAL(triggered()), this, SLOT(diffProject()));
380     cvsMenu->addAction(command);
381     m_commandLocator->appendCommand(command);
382
383     m_statusProjectAction = new Utils::ParameterAction(tr("Project Status"), tr("Status of Project \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
384     command = ami->registerAction(m_statusProjectAction, CMD_ID_STATUS,
385         globalcontext);
386     command->setAttribute(Core::Command::CA_UpdateText);
387     connect(m_statusProjectAction, SIGNAL(triggered()), this, SLOT(projectStatus()));
388     cvsMenu->addAction(command);
389     m_commandLocator->appendCommand(command);
390
391     m_logProjectAction = new Utils::ParameterAction(tr("Log Project"), tr("Log Project \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
392     command = ami->registerAction(m_logProjectAction, CMD_ID_PROJECTLOG, globalcontext);
393     command->setAttribute(Core::Command::CA_UpdateText);
394     connect(m_logProjectAction, SIGNAL(triggered()), this, SLOT(logProject()));
395     cvsMenu->addAction(command);
396     m_commandLocator->appendCommand(command);
397
398     m_updateProjectAction = new Utils::ParameterAction(tr("Update Project"), tr("Update Project \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
399     command = ami->registerAction(m_updateProjectAction, CMD_ID_UPDATE, globalcontext);
400     command->setAttribute(Core::Command::CA_UpdateText);
401     connect(m_updateProjectAction, SIGNAL(triggered()), this, SLOT(updateProject()));
402     cvsMenu->addAction(command);
403     m_commandLocator->appendCommand(command);
404
405     m_commitProjectAction = new Utils::ParameterAction(tr("Commit Project"), tr("Commit Project \"%1\""), Utils::ParameterAction::EnabledWithParameter, this);
406     command = ami->registerAction(m_commitProjectAction, CMD_ID_PROJECTCOMMIT, globalcontext);
407     command->setAttribute(Core::Command::CA_UpdateText);
408     connect(m_commitProjectAction, SIGNAL(triggered()), this, SLOT(commitProject()));
409     cvsMenu->addAction(command);
410     m_commandLocator->appendCommand(command);
411
412     cvsMenu->addAction(createSeparator(this, ami, CMD_ID_SEPARATOR3, globalcontext));
413
414     m_diffRepositoryAction = new QAction(tr("Diff Repository"), this);
415     command = ami->registerAction(m_diffRepositoryAction, CMD_ID_REPOSITORYDIFF, globalcontext);
416     connect(m_diffRepositoryAction, SIGNAL(triggered()), this, SLOT(diffRepository()));
417     cvsMenu->addAction(command);
418     m_commandLocator->appendCommand(command);
419
420     m_statusRepositoryAction = new QAction(tr("Repository Status"), this);
421     command = ami->registerAction(m_statusRepositoryAction, CMD_ID_REPOSITORYSTATUS, globalcontext);
422     connect(m_statusRepositoryAction, SIGNAL(triggered()), this, SLOT(statusRepository()));
423     cvsMenu->addAction(command);
424     m_commandLocator->appendCommand(command);
425
426     m_logRepositoryAction = new QAction(tr("Repository Log"), this);
427     command = ami->registerAction(m_logRepositoryAction, CMD_ID_REPOSITORYLOG, globalcontext);
428     connect(m_logRepositoryAction, SIGNAL(triggered()), this, SLOT(logRepository()));
429     cvsMenu->addAction(command);
430     m_commandLocator->appendCommand(command);
431
432     m_updateRepositoryAction = new QAction(tr("Update Repository"), this);
433     command = ami->registerAction(m_updateRepositoryAction, CMD_ID_REPOSITORYUPDATE, globalcontext);
434     connect(m_updateRepositoryAction, SIGNAL(triggered()), this, SLOT(updateRepository()));
435     cvsMenu->addAction(command);
436     m_commandLocator->appendCommand(command);
437
438     m_commitAllAction = new QAction(tr("Commit All Files"), this);
439     command = ami->registerAction(m_commitAllAction, CMD_ID_COMMIT_ALL,
440         globalcontext);
441     connect(m_commitAllAction, SIGNAL(triggered()), this, SLOT(startCommitAll()));
442     cvsMenu->addAction(command);
443     m_commandLocator->appendCommand(command);
444
445     m_revertRepositoryAction = new QAction(tr("Revert Repository..."), this);
446     command = ami->registerAction(m_revertRepositoryAction, CMD_ID_REVERT_ALL,
447                                   globalcontext);
448     connect(m_revertRepositoryAction, SIGNAL(triggered()), this, SLOT(revertAll()));
449     cvsMenu->addAction(command);
450     m_commandLocator->appendCommand(command);
451
452     // Actions of the submit editor
453     Core::Context cvscommitcontext(Constants::CVSCOMMITEDITOR);
454
455     m_submitCurrentLogAction = new QAction(VCSBase::VCSBaseSubmitEditor::submitIcon(), tr("Commit"), this);
456     command = ami->registerAction(m_submitCurrentLogAction, Constants::SUBMIT_CURRENT, cvscommitcontext);
457     command->setAttribute(Core::Command::CA_UpdateText);
458     connect(m_submitCurrentLogAction, SIGNAL(triggered()), this, SLOT(submitCurrentLog()));
459
460     m_submitDiffAction = new QAction(VCSBase::VCSBaseSubmitEditor::diffIcon(), tr("Diff Selected Files"), this);
461     command = ami->registerAction(m_submitDiffAction , Constants::DIFF_SELECTED, cvscommitcontext);
462
463     m_submitUndoAction = new QAction(tr("&Undo"), this);
464     command = ami->registerAction(m_submitUndoAction, Core::Constants::UNDO, cvscommitcontext);
465
466     m_submitRedoAction = new QAction(tr("&Redo"), this);
467     command = ami->registerAction(m_submitRedoAction, Core::Constants::REDO, cvscommitcontext);
468     return true;
469 }
470
471 bool CVSPlugin::submitEditorAboutToClose(VCSBase::VCSBaseSubmitEditor *submitEditor)
472 {
473     if (!isCommitEditorOpen())
474         return true;
475
476     Core::IFile *fileIFace = submitEditor->file();
477     const CVSSubmitEditor *editor = qobject_cast<CVSSubmitEditor *>(submitEditor);
478     if (!fileIFace || !editor)
479         return true;
480
481     // Submit editor closing. Make it write out the commit message
482     // and retrieve files
483     const QFileInfo editorFile(fileIFace->fileName());
484     const QFileInfo changeFile(m_commitMessageFileName);
485     if (editorFile.absoluteFilePath() != changeFile.absoluteFilePath())
486         return true; // Oops?!
487
488     // Prompt user. Force a prompt unless submit was actually invoked (that
489     // is, the editor was closed or shutdown).
490     CVSSettings newSettings = m_settings;
491     const VCSBase::VCSBaseSubmitEditor::PromptSubmitResult answer =
492             editor->promptSubmit(tr("Closing CVS Editor"),
493                                  tr("Do you want to commit the change?"),
494                                  tr("The commit message check failed. Do you want to commit the change?"),
495                                  &newSettings.promptToSubmit, !m_submitActionTriggered);
496     m_submitActionTriggered = false;
497     switch (answer) {
498     case VCSBase::VCSBaseSubmitEditor::SubmitCanceled:
499         return false; // Keep editing and change file
500     case VCSBase::VCSBaseSubmitEditor::SubmitDiscarded:
501         cleanCommitMessageFile();
502         return true; // Cancel all
503     default:
504         break;
505     }
506     setSettings(newSettings); // in case someone turned prompting off
507     const QStringList fileList = editor->checkedFiles();
508     bool closeEditor = true;
509     if (!fileList.empty()) {
510         // get message & commit
511         Core::ICore::instance()->fileManager()->blockFileChange(fileIFace);
512         fileIFace->save();
513         Core::ICore::instance()->fileManager()->unblockFileChange(fileIFace);
514         closeEditor= commit(m_commitMessageFileName, fileList);
515     }
516     if (closeEditor)
517         cleanCommitMessageFile();
518     return closeEditor;
519 }
520
521 void CVSPlugin::diffCommitFiles(const QStringList &files)
522 {
523     cvsDiff(m_commitRepository, files);
524 }
525
526 static inline void setDiffBaseDirectory(Core::IEditor *editor, const QString &db)
527 {
528     if (VCSBase::VCSBaseEditor *ve = qobject_cast<VCSBase::VCSBaseEditor*>(editor->widget()))
529         ve->setDiffBaseDirectory(db);
530 }
531
532 void CVSPlugin::cvsDiff(const QString &workingDir, const QStringList &files)
533 {
534     if (CVS::Constants::debug)
535         qDebug() << Q_FUNC_INFO << files;
536     const QString source = VCSBase::VCSBaseEditor::getSource(workingDir, files);
537     QTextCodec *codec = VCSBase::VCSBaseEditor::getCodec(workingDir, files);
538     const QString id = VCSBase::VCSBaseEditor::getTitleId(workingDir, files);
539
540     QStringList args(QLatin1String("diff"));
541     args << m_settings.cvsDiffOptions;
542     args.append(files);
543
544     // CVS returns the diff exit code (1 if files differ), which is
545     // undistinguishable from a "file not found" error, unfortunately.
546     const CVSResponse response =
547             runCVS(workingDir, args, m_settings.timeOutMS(), 0, codec);
548     switch (response.result) {
549     case CVSResponse::NonNullExitCode:
550     case CVSResponse::Ok:
551         break;
552     case CVSResponse::OtherError:
553         return;
554     }
555
556     QString output = fixDiffOutput(response.stdOut);
557     if (output.isEmpty())
558         output = tr("The files do not differ.");
559     // diff of a single file? re-use an existing view if possible to support
560     // the common usage pattern of continuously changing and diffing a file
561     if (files.count() == 1) {
562         // Show in the same editor if diff has been executed before
563         if (Core::IEditor *editor = locateEditor("originalFileName", id)) {
564             editor->createNew(output);
565             Core::EditorManager::instance()->activateEditor(editor, Core::EditorManager::ModeSwitch);
566             setDiffBaseDirectory(editor, workingDir);
567             return;
568         }
569     }
570     const QString title = QString::fromLatin1("cvs diff %1").arg(id);
571     Core::IEditor *editor = showOutputInEditor(title, output, VCSBase::DiffOutput, source, codec);
572     if (files.count() == 1)
573         editor->setProperty("originalFileName", id);
574     setDiffBaseDirectory(editor, workingDir);
575 }
576
577 CVSSubmitEditor *CVSPlugin::openCVSSubmitEditor(const QString &fileName)
578 {
579     Core::IEditor *editor = Core::EditorManager::instance()->openEditor(fileName, QLatin1String(Constants::CVSCOMMITEDITOR_ID),
580                                                                         Core::EditorManager::ModeSwitch);
581     CVSSubmitEditor *submitEditor = qobject_cast<CVSSubmitEditor*>(editor);
582     QTC_ASSERT(submitEditor, /**/);
583     submitEditor->registerActions(m_submitUndoAction, m_submitRedoAction, m_submitCurrentLogAction, m_submitDiffAction);
584     connect(submitEditor, SIGNAL(diffSelectedFiles(QStringList)), this, SLOT(diffCommitFiles(QStringList)));
585
586     return submitEditor;
587 }
588
589 void CVSPlugin::updateActions(VCSBase::VCSBasePlugin::ActionState as)
590 {
591     if (!enableMenuAction(as, m_menuAction)) {
592         m_commandLocator->setEnabled(false);
593         return;
594     }
595
596     const bool hasTopLevel = currentState().hasTopLevel();
597     m_commandLocator->setEnabled(hasTopLevel);
598
599     const QString currentFileName = currentState().currentFileName();
600     m_addAction->setParameter(currentFileName);
601     m_deleteAction->setParameter(currentFileName);
602     m_revertAction->setParameter(currentFileName);
603     m_diffCurrentAction->setParameter(currentFileName);
604     m_commitCurrentAction->setParameter(currentFileName);
605     m_filelogCurrentAction->setParameter(currentFileName);
606     m_annotateCurrentAction->setParameter(currentFileName);
607     m_editCurrentAction->setParameter(currentFileName);
608     m_uneditCurrentAction->setParameter(currentFileName);
609
610     const QString currentProjectName = currentState().currentProjectName();
611     m_diffProjectAction->setParameter(currentProjectName);
612     m_statusProjectAction->setParameter(currentProjectName);
613     m_updateProjectAction->setParameter(currentProjectName);
614     m_logProjectAction->setParameter(currentProjectName);
615     m_commitProjectAction->setParameter(currentProjectName);
616
617     m_diffRepositoryAction->setEnabled(hasTopLevel);
618     m_statusRepositoryAction->setEnabled(hasTopLevel);
619     m_updateRepositoryAction->setEnabled(hasTopLevel);
620     m_commitAllAction->setEnabled(hasTopLevel);
621     m_logRepositoryAction->setEnabled(hasTopLevel);
622     m_uneditRepositoryAction->setEnabled(hasTopLevel);
623 }
624
625 void CVSPlugin::addCurrentFile()
626 {
627     const VCSBase::VCSBasePluginState state = currentState();
628     QTC_ASSERT(state.hasFile(), return)
629     vcsAdd(state.currentFileTopLevel(), state.relativeCurrentFile());
630 }
631
632 void CVSPlugin::revertAll()
633 {
634     const VCSBase::VCSBasePluginState state = currentState();
635     QTC_ASSERT(state.hasTopLevel(), return)
636     const QString title = tr("Revert repository");
637     if (!messageBoxQuestion(title, tr("Revert all pending changes to the repository?")))
638         return;
639     QStringList args;
640     args << QLatin1String("update") << QLatin1String("-C") << state.topLevel();
641     const CVSResponse revertResponse =
642             runCVS(state.topLevel(), args, m_settings.timeOutMS(),
643                    SshPasswordPrompt|ShowStdOutInLogWindow);
644     if (revertResponse.result == CVSResponse::Ok) {
645         cvsVersionControl()->emitRepositoryChanged(state.topLevel());
646     } else {
647         QMessageBox::warning(0, title, tr("Revert failed: %1").arg(revertResponse.message), QMessageBox::Ok);
648     }
649 }
650
651 void CVSPlugin::revertCurrentFile()
652 {
653     const VCSBase::VCSBasePluginState state = currentState();
654     QTC_ASSERT(state.hasFile(), return)
655     QStringList args;
656     args << QLatin1String("diff") << state.relativeCurrentFile();
657     const CVSResponse diffResponse =
658             runCVS(state.currentFileTopLevel(), args, m_settings.timeOutMS(), 0);
659     switch (diffResponse.result) {
660     case CVSResponse::Ok:
661         return; // Not modified, diff exit code 0
662     case CVSResponse::NonNullExitCode: // Diff exit code != 0
663         if (diffResponse.stdOut.isEmpty()) // Paranoia: Something else failed?
664             return;
665         break;
666     case CVSResponse::OtherError:
667         return;
668     }
669
670     if (!messageBoxQuestion(QLatin1String("CVS Revert"),
671                             tr("The file has been changed. Do you want to revert it?")))
672         return;
673
674     Core::FileChangeBlocker fcb(state.currentFile());
675
676     // revert
677     args.clear();
678     args << QLatin1String("update") << QLatin1String("-C") << state.relativeCurrentFile();
679     const CVSResponse revertResponse =
680             runCVS(state.currentFileTopLevel(), args, m_settings.timeOutMS(),
681                    SshPasswordPrompt|ShowStdOutInLogWindow);
682     if (revertResponse.result == CVSResponse::Ok) {
683         cvsVersionControl()->emitFilesChanged(QStringList(state.currentFile()));
684     }
685 }
686
687 void CVSPlugin::diffProject()
688 {
689     const VCSBase::VCSBasePluginState state = currentState();
690     QTC_ASSERT(state.hasProject(), return)
691     cvsDiff(state.currentProjectTopLevel(), state.relativeCurrentProject());
692 }
693
694 void CVSPlugin::diffCurrentFile()
695 {
696     const VCSBase::VCSBasePluginState state = currentState();
697     QTC_ASSERT(state.hasFile(), return)
698     cvsDiff(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()));
699 }
700
701 void CVSPlugin::startCommitCurrentFile()
702 {
703     const VCSBase::VCSBasePluginState state = currentState();
704     QTC_ASSERT(state.hasFile(), return)
705     startCommit(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()));
706 }
707
708 void CVSPlugin::startCommitAll()
709 {
710     const VCSBase::VCSBasePluginState state = currentState();
711     QTC_ASSERT(state.hasTopLevel(), return)
712     startCommit(state.topLevel());
713 }
714
715 /* Start commit of files of a single repository by displaying
716  * template and files in a submit editor. On closing, the real
717  * commit will start. */
718 void CVSPlugin::startCommit(const QString &workingDir, const QStringList &files)
719 {
720     if (VCSBase::VCSBaseSubmitEditor::raiseSubmitEditor())
721         return;
722     if (isCommitEditorOpen()) {
723         VCSBase::VCSBaseOutputWindow::instance()->appendWarning(tr("Another commit is currently being executed."));
724         return;
725     }
726
727     // We need the "Examining <subdir>" stderr output to tell
728     // where we are, so, have stdout/stderr channels merged.
729     QStringList args = QStringList(QLatin1String("status"));
730     const CVSResponse response =
731             runCVS(workingDir, args, m_settings.timeOutMS(), MergeOutputChannels);
732     if (response.result != CVSResponse::Ok)
733         return;
734     // Get list of added/modified/deleted files and purge out undesired ones
735     // (do not run status with relative arguments as it will omit the directories)
736     StateList statusOutput = parseStatusOutput(QString(), response.stdOut);
737     if (!files.isEmpty()) {
738         for (StateList::iterator it = statusOutput.begin(); it != statusOutput.end() ; ) {
739             if (files.contains(it->second)) {
740                 ++it;
741             } else {
742                 it = statusOutput.erase(it);
743             }
744         }
745     }
746     if (statusOutput.empty()) {
747         VCSBase::VCSBaseOutputWindow::instance()->append(tr("There are no modified files."));
748         return;
749     }
750     m_commitRepository = workingDir;
751
752     // Create a new submit change file containing the submit template
753     QTemporaryFile changeTmpFile;
754     changeTmpFile.setAutoRemove(false);
755     if (!changeTmpFile.open()) {
756         VCSBase::VCSBaseOutputWindow::instance()->appendError(tr("Cannot create temporary file: %1").arg(changeTmpFile.errorString()));
757         return;
758     }
759     // TODO: Retrieve submit template from
760     const QString submitTemplate;
761     m_commitMessageFileName = changeTmpFile.fileName();
762     // Create a submit
763     changeTmpFile.write(submitTemplate.toUtf8());
764     changeTmpFile.flush();
765     changeTmpFile.close();
766     // Create a submit editor and set file list
767     CVSSubmitEditor *editor = openCVSSubmitEditor(m_commitMessageFileName);
768     editor->setCheckScriptWorkingDirectory(m_commitRepository);
769     editor->setStateList(statusOutput);
770 }
771
772 bool CVSPlugin::commit(const QString &messageFile,
773                               const QStringList &fileList)
774 {
775     if (CVS::Constants::debug)
776         qDebug() << Q_FUNC_INFO << messageFile << fileList;
777     QStringList args = QStringList(QLatin1String("commit"));
778     args << QLatin1String("-F") << messageFile;
779     args.append(fileList);
780     const CVSResponse response =
781             runCVS(m_commitRepository, args, m_settings.longTimeOutMS(),
782                    SshPasswordPrompt|ShowStdOutInLogWindow);
783     return response.result == CVSResponse::Ok ;
784 }
785
786 void CVSPlugin::filelogCurrentFile()
787 {
788     const VCSBase::VCSBasePluginState state = currentState();
789     QTC_ASSERT(state.hasFile(), return)
790     filelog(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()), true);
791 }
792
793 void CVSPlugin::logProject()
794 {
795     const VCSBase::VCSBasePluginState state = currentState();
796     QTC_ASSERT(state.hasProject(), return)
797     filelog(state.currentProjectTopLevel(), state.relativeCurrentProject());
798 }
799
800 void CVSPlugin::logRepository()
801 {
802     const VCSBase::VCSBasePluginState state = currentState();
803     QTC_ASSERT(state.hasTopLevel(), return)
804     filelog(state.topLevel());
805 }
806
807 void CVSPlugin::filelog(const QString &workingDir,
808                         const QStringList &files,
809                         bool enableAnnotationContextMenu)
810 {
811     QTextCodec *codec = VCSBase::VCSBaseEditor::getCodec(workingDir, files);
812     // no need for temp file
813     const QString id = VCSBase::VCSBaseEditor::getTitleId(workingDir, files);
814     const QString source = VCSBase::VCSBaseEditor::getSource(workingDir, files);
815     QStringList args;
816     args << QLatin1String("log");
817     args.append(files);
818     const CVSResponse response =
819             runCVS(workingDir, args, m_settings.timeOutMS(),
820                    SshPasswordPrompt, codec);
821     if (response.result != CVSResponse::Ok)
822         return;
823
824     // Re-use an existing view if possible to support
825     // the common usage pattern of continuously changing and diffing a file
826     if (Core::IEditor *editor = locateEditor("logFileName", id)) {
827         editor->createNew(response.stdOut);
828         Core::EditorManager::instance()->activateEditor(editor, Core::EditorManager::ModeSwitch);
829     } else {
830         const QString title = QString::fromLatin1("cvs log %1").arg(id);
831         Core::IEditor *newEditor = showOutputInEditor(title, response.stdOut, VCSBase::LogOutput, source, codec);
832         newEditor->setProperty("logFileName", id);
833         if (enableAnnotationContextMenu)
834             VCSBase::VCSBaseEditor::getVcsBaseEditor(newEditor)->setFileLogAnnotateEnabled(true);
835     }
836 }
837
838 void CVSPlugin::updateProject()
839 {
840     const VCSBase::VCSBasePluginState state = currentState();
841     QTC_ASSERT(state.hasProject(), return)
842     update(state.currentProjectTopLevel(), state.relativeCurrentProject());
843 }
844
845 bool CVSPlugin::update(const QString &topLevel, const QStringList &files)
846 {
847     QStringList args(QLatin1String("update"));
848     args.push_back(QLatin1String("-dR"));
849     args.append(files);
850     const CVSResponse response =
851             runCVS(topLevel, args, m_settings.longTimeOutMS(),
852                    SshPasswordPrompt|ShowStdOutInLogWindow);
853     const bool ok = response.result == CVSResponse::Ok;
854     if (ok)
855         cvsVersionControl()->emitRepositoryChanged(topLevel);
856     return ok;
857 }
858
859 void CVSPlugin::editCurrentFile()
860 {
861     const VCSBase::VCSBasePluginState state = currentState();
862     QTC_ASSERT(state.hasFile(), return)
863     edit(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()));
864 }
865
866 void CVSPlugin::uneditCurrentFile()
867 {
868     const VCSBase::VCSBasePluginState state = currentState();
869     QTC_ASSERT(state.hasFile(), return)
870     unedit(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()));
871 }
872
873 void CVSPlugin::uneditCurrentRepository()
874 {
875     const VCSBase::VCSBasePluginState state = currentState();
876     QTC_ASSERT(state.hasTopLevel(), return)
877     unedit(state.topLevel(), QStringList());
878 }
879
880 void CVSPlugin::annotateCurrentFile()
881 {
882     const VCSBase::VCSBasePluginState state = currentState();
883     QTC_ASSERT(state.hasFile(), return)
884     annotate(state.currentFileTopLevel(), state.relativeCurrentFile());
885 }
886
887 void CVSPlugin::vcsAnnotate(const QString &file, const QString &revision, int lineNumber)
888 {
889     const QFileInfo fi(file);
890     annotate(fi.absolutePath(), fi.fileName(), revision, lineNumber);
891 }
892
893 bool CVSPlugin::edit(const QString &topLevel, const QStringList &files)
894 {
895     QStringList args(QLatin1String("edit"));
896     args.append(files);
897     const CVSResponse response =
898             runCVS(topLevel, args, m_settings.timeOutMS(),
899                    ShowStdOutInLogWindow|SshPasswordPrompt);
900     return response.result == CVSResponse::Ok;
901 }
902
903 bool CVSPlugin::diffCheckModified(const QString &topLevel, const QStringList &files, bool *modified)
904 {
905     // Quick check for modified files using diff
906     *modified = false;
907     QStringList args(QLatin1String("-q"));
908     args << QLatin1String("diff");
909     args.append(files);
910     const CVSResponse response = runCVS(topLevel, args, m_settings.timeOutMS(), 0);
911     if (response.result == CVSResponse::OtherError)
912         return false;
913     *modified = response.result == CVSResponse::NonNullExitCode;
914     return true;
915 }
916
917 bool CVSPlugin::unedit(const QString &topLevel, const QStringList &files)
918 {
919     bool modified;
920     // Prompt and use force flag if modified
921     if (!diffCheckModified(topLevel, files, &modified))
922         return false;
923     if (modified) {
924         const QString question = files.isEmpty() ?
925                       tr("Would you like to discard your changes to the repository '%1'?").arg(topLevel) :
926                       tr("Would you like to discard your changes to the file '%1'?").arg(files.front());
927         if (!messageBoxQuestion(tr("Unedit"), question))
928             return false;
929     }
930
931     QStringList args(QLatin1String("unedit"));
932     // Note: Option '-y' to force 'yes'-answer to CVS' 'undo change' prompt,
933     // exists in CVSNT only as of 6.8.2010. Standard CVS will otherwise prompt
934     if (modified)
935         args.append(QLatin1String("-y"));
936     args.append(files);
937     const CVSResponse response =
938             runCVS(topLevel, args, m_settings.timeOutMS(),
939                    ShowStdOutInLogWindow|SshPasswordPrompt);
940     return response.result == CVSResponse::Ok;
941 }
942
943 void CVSPlugin::annotate(const QString &workingDir, const QString &file,
944                          const QString &revision /* = QString() */,
945                          int lineNumber /* = -1 */)
946 {
947     const QStringList files(file);
948     QTextCodec *codec = VCSBase::VCSBaseEditor::getCodec(workingDir, files);
949     const QString id = VCSBase::VCSBaseEditor::getTitleId(workingDir, files, revision);
950     const QString source = VCSBase::VCSBaseEditor::getSource(workingDir, file);
951     QStringList args;
952     args << QLatin1String("annotate");
953     if (!revision.isEmpty())
954         args << QLatin1String("-r") << revision;
955     args << file;
956     const CVSResponse response =
957             runCVS(workingDir, args, m_settings.timeOutMS(),
958                    SshPasswordPrompt, codec);
959     if (response.result != CVSResponse::Ok)
960         return;
961
962     // Re-use an existing view if possible to support
963     // the common usage pattern of continuously changing and diffing a file
964     if (lineNumber < 1)
965         lineNumber = VCSBase::VCSBaseEditor::lineNumberOfCurrentEditor(file);
966
967     if (Core::IEditor *editor = locateEditor("annotateFileName", id)) {
968         editor->createNew(response.stdOut);
969         VCSBase::VCSBaseEditor::gotoLineOfEditor(editor, lineNumber);
970         Core::EditorManager::instance()->activateEditor(editor, Core::EditorManager::ModeSwitch);
971     } else {
972         const QString title = QString::fromLatin1("cvs annotate %1").arg(id);
973         Core::IEditor *newEditor = showOutputInEditor(title, response.stdOut, VCSBase::AnnotateOutput, source, codec);
974         newEditor->setProperty("annotateFileName", id);
975         VCSBase::VCSBaseEditor::gotoLineOfEditor(newEditor, lineNumber);
976     }
977 }
978
979 bool CVSPlugin::status(const QString &topLevel, const QStringList &files, const QString &title)
980 {
981     QStringList args(QLatin1String("status"));
982     args.append(files);
983     const CVSResponse response =
984             runCVS(topLevel, args, m_settings.timeOutMS(), 0);
985     const bool ok = response.result == CVSResponse::Ok;
986     if (ok)
987         showOutputInEditor(title, response.stdOut, VCSBase::RegularCommandOutput, topLevel, 0);
988     return ok;
989 }
990
991 void CVSPlugin::projectStatus()
992 {
993     const VCSBase::VCSBasePluginState state = currentState();
994     QTC_ASSERT(state.hasProject(), return)
995     status(state.currentProjectTopLevel(), state.relativeCurrentProject(), tr("Project status"));
996 }
997
998 void CVSPlugin::commitProject()
999 {
1000     const VCSBase::VCSBasePluginState state = currentState();
1001     QTC_ASSERT(state.hasProject(), return)
1002     startCommit(state.currentProjectTopLevel(), state.relativeCurrentProject());
1003 }
1004
1005 void CVSPlugin::diffRepository()
1006 {
1007     const VCSBase::VCSBasePluginState state = currentState();
1008     QTC_ASSERT(state.hasTopLevel(), return)
1009     cvsDiff(state.topLevel(), QStringList());
1010 }
1011
1012 void CVSPlugin::statusRepository()
1013 {
1014     const VCSBase::VCSBasePluginState state = currentState();
1015     QTC_ASSERT(state.hasTopLevel(), return)
1016     status(state.topLevel(), QStringList(), tr("Repository status"));
1017 }
1018
1019 void CVSPlugin::updateRepository()
1020 {
1021     const VCSBase::VCSBasePluginState state = currentState();
1022     QTC_ASSERT(state.hasTopLevel(), return)
1023     update(state.topLevel(), QStringList());
1024
1025 }
1026
1027 void CVSPlugin::slotDescribe(const QString &source, const QString &changeNr)
1028 {
1029     QString errorMessage;
1030     if (!describe(source, changeNr, &errorMessage))
1031         VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage);
1032 }
1033
1034 bool CVSPlugin::describe(const QString &file, const QString &changeNr, QString *errorMessage)
1035 {
1036
1037     QString toplevel;
1038     const bool manages = managesDirectory(QFileInfo(file).absolutePath(), &toplevel);
1039     if (!manages || toplevel.isEmpty()) {
1040         *errorMessage = msgCannotFindTopLevel(file);
1041         return false;
1042     }
1043     return describe(toplevel, QDir(toplevel).relativeFilePath(file), changeNr, errorMessage);
1044 }
1045
1046 bool CVSPlugin::describe(const QString &toplevel, const QString &file, const
1047                          QString &changeNr, QString *errorMessage)
1048 {
1049
1050     // In CVS, revisions of files are normally unrelated, there is
1051     // no global revision/change number. The only thing that groups
1052     // a commit is the "commit-id" (as shown in the log).
1053     // This function makes use of it to find all files related to
1054     // a commit in order to emulate a "describe global change" functionality
1055     // if desired.
1056     if (CVS::Constants::debug)
1057         qDebug() << Q_FUNC_INFO << file << changeNr;
1058     // Number must be > 1
1059     if (isFirstRevision(changeNr)) {
1060         *errorMessage = tr("The initial revision %1 cannot be described.").arg(changeNr);
1061         return false;
1062     }
1063     // Run log to obtain commit id and details
1064     QStringList args;
1065     args << QLatin1String("log") << (QLatin1String("-r") + changeNr) << file;
1066     const CVSResponse logResponse =
1067             runCVS(toplevel, args, m_settings.timeOutMS(), SshPasswordPrompt);
1068     if (logResponse.result != CVSResponse::Ok) {
1069         *errorMessage = logResponse.message;
1070         return false;
1071     }
1072     const QList<CVS_LogEntry> fileLog = parseLogEntries(logResponse.stdOut);
1073     if (fileLog.empty() || fileLog.front().revisions.empty()) {
1074         *errorMessage = msgLogParsingFailed();
1075         return false;
1076     }
1077     if (m_settings.describeByCommitId) {
1078         // Run a log command over the repo, filtering by the commit date
1079         // and commit id, collecting all files touched by the commit.
1080         const QString commitId = fileLog.front().revisions.front().commitId;
1081         // Date range "D1<D2" in ISO format "YYYY-MM-DD"
1082         const QString dateS = fileLog.front().revisions.front().date;
1083         const QDate date = QDate::fromString(dateS, Qt::ISODate);
1084         const QString nextDayS = date.addDays(1).toString(Qt::ISODate);
1085         args.clear();
1086         args << QLatin1String("log") << QLatin1String("-d") << (dateS  + QLatin1Char('<') + nextDayS);
1087
1088         const CVSResponse repoLogResponse =
1089                 runCVS(toplevel, args, m_settings.longTimeOutMS(), SshPasswordPrompt);
1090         if (repoLogResponse.result != CVSResponse::Ok) {
1091             *errorMessage = repoLogResponse.message;
1092             return false;
1093         }
1094         // Describe all files found, pass on dir to obtain correct absolute paths.
1095         const QList<CVS_LogEntry> repoEntries = parseLogEntries(repoLogResponse.stdOut, QString(), commitId);
1096         if (repoEntries.empty()) {
1097             *errorMessage = tr("Could not find commits of id '%1' on %2.").arg(commitId, dateS);
1098             return false;
1099         }
1100         return describe(toplevel, repoEntries, errorMessage);
1101     } else {
1102         // Just describe that single entry
1103         return describe(toplevel, fileLog, errorMessage);
1104     }
1105     return false;
1106 }
1107
1108 // Describe a set of files and revisions by
1109 // concatenating log and diffs to previous revisions
1110 bool CVSPlugin::describe(const QString &repositoryPath,
1111                          QList<CVS_LogEntry> entries,
1112                          QString *errorMessage)
1113 {
1114     // Collect logs
1115     QString output;
1116     QTextCodec *codec = 0;
1117     const QList<CVS_LogEntry>::iterator lend = entries.end();
1118     for (QList<CVS_LogEntry>::iterator it = entries.begin(); it != lend; ++it) {
1119         // Before fiddling file names, try to find codec
1120         if (!codec)
1121             codec = VCSBase::VCSBaseEditor::getCodec(repositoryPath, QStringList(it->file));
1122         // Run log
1123         QStringList args(QLatin1String("log"));
1124         args << (QLatin1String("-r") + it->revisions.front().revision) << it->file;
1125         const CVSResponse logResponse =
1126                 runCVS(repositoryPath, args, m_settings.timeOutMS(), SshPasswordPrompt);
1127         if (logResponse.result != CVSResponse::Ok) {
1128             *errorMessage =  logResponse.message;
1129             return false;
1130         }
1131         output += logResponse.stdOut;
1132     }
1133     // Collect diffs relative to repository
1134     for (QList<CVS_LogEntry>::iterator it = entries.begin(); it != lend; ++it) {
1135         const QString &revision = it->revisions.front().revision;
1136         if (!isFirstRevision(revision)) {
1137             const QString previousRev = previousRevision(revision);
1138             QStringList args(QLatin1String("diff"));
1139             args << m_settings.cvsDiffOptions << QLatin1String("-r") << previousRev
1140                     << QLatin1String("-r") << it->revisions.front().revision
1141                     << it->file;
1142             const CVSResponse diffResponse =
1143                     runCVS(repositoryPath, args, m_settings.timeOutMS(), 0, codec);
1144             switch (diffResponse.result) {
1145             case CVSResponse::Ok:
1146             case CVSResponse::NonNullExitCode: // Diff exit code != 0
1147                 if (diffResponse.stdOut.isEmpty()) {
1148                     *errorMessage = diffResponse.message;
1149                     return false; // Something else failed.
1150                 }
1151                 break;
1152             case CVSResponse::OtherError:
1153                 *errorMessage = diffResponse.message;
1154                 return false;
1155             }
1156             output += fixDiffOutput(diffResponse.stdOut);
1157         }
1158     }
1159
1160     // Re-use an existing view if possible to support
1161     // the common usage pattern of continuously changing and diffing a file
1162     const QString commitId = entries.front().revisions.front().commitId;
1163     if (Core::IEditor *editor = locateEditor("describeChange", commitId)) {
1164         editor->createNew(output);
1165         Core::EditorManager::instance()->activateEditor(editor, Core::EditorManager::ModeSwitch);
1166         setDiffBaseDirectory(editor, repositoryPath);
1167     } else {
1168         const QString title = QString::fromLatin1("cvs describe %1").arg(commitId);
1169         Core::IEditor *newEditor = showOutputInEditor(title, output, VCSBase::DiffOutput, entries.front().file, codec);
1170         newEditor->setProperty("describeChange", commitId);
1171         setDiffBaseDirectory(newEditor, repositoryPath);
1172     }
1173     return true;
1174 }
1175
1176 void CVSPlugin::submitCurrentLog()
1177 {
1178     m_submitActionTriggered = true;
1179     Core::EditorManager::instance()->closeEditors(QList<Core::IEditor*>()
1180         << Core::EditorManager::instance()->currentEditor());
1181 }
1182
1183 // Run CVS. At this point, file arguments must be relative to
1184 // the working directory (see above).
1185 CVSResponse CVSPlugin::runCVS(const QString &workingDirectory,
1186                               const QStringList &arguments,
1187                               int timeOut,
1188                               unsigned flags,
1189                               QTextCodec *outputCodec)
1190 {
1191     const QString executable = m_settings.cvsCommand;
1192     CVSResponse response;
1193     if (executable.isEmpty()) {
1194         response.result = CVSResponse::OtherError;
1195         response.message =tr("No cvs executable specified!");
1196         return response;
1197     }
1198     // Run, connect stderr to the output window
1199     const Utils::SynchronousProcessResponse sp_resp =
1200             runVCS(workingDirectory, executable,
1201                    m_settings.addOptions(arguments),
1202                    timeOut, flags, outputCodec);
1203
1204     response.result = CVSResponse::OtherError;
1205     response.stdErr = sp_resp.stdErr;
1206     response.stdOut = sp_resp.stdOut;
1207     switch (sp_resp.result) {
1208     case Utils::SynchronousProcessResponse::Finished:
1209         response.result = CVSResponse::Ok;
1210         break;
1211     case Utils::SynchronousProcessResponse::FinishedError:
1212         response.result = CVSResponse::NonNullExitCode;
1213         break;
1214     case Utils::SynchronousProcessResponse::TerminatedAbnormally:
1215     case Utils::SynchronousProcessResponse::StartFailed:
1216     case Utils::SynchronousProcessResponse::Hang:
1217         break;
1218     }
1219
1220     if (response.result != CVSResponse::Ok)
1221         response.message = sp_resp.exitMessage(executable, timeOut);
1222
1223     return response;
1224 }
1225
1226 Core::IEditor * CVSPlugin::showOutputInEditor(const QString& title, const QString &output,
1227                                                      int editorType, const QString &source,
1228                                                      QTextCodec *codec)
1229 {
1230     const VCSBase::VCSBaseEditorParameters *params = findType(editorType);
1231     QTC_ASSERT(params, return 0);
1232     const QString id = params->id;
1233     if (CVS::Constants::debug)
1234         qDebug() << "CVSPlugin::showOutputInEditor" << title << id <<  "source=" << source << "Size= " << output.size() <<  " Type=" << editorType << debugCodec(codec);
1235     QString s = title;
1236     Core::IEditor *editor = Core::EditorManager::instance()->openEditorWithContents(id, &s, output.toLocal8Bit());
1237     connect(editor, SIGNAL(annotateRevisionRequested(QString,QString,int)),
1238             this, SLOT(vcsAnnotate(QString,QString,int)));
1239     CVSEditor *e = qobject_cast<CVSEditor*>(editor->widget());
1240     if (!e)
1241         return 0;
1242     s.replace(QLatin1Char(' '), QLatin1Char('_'));
1243     e->setSuggestedFileName(s);
1244     e->setForceReadOnly(true);
1245     if (!source.isEmpty())
1246         e->setSource(source);
1247     if (codec)
1248         e->setCodec(codec);
1249     Core::IEditor *ie = e->editableInterface();
1250     Core::EditorManager::instance()->activateEditor(ie, Core::EditorManager::ModeSwitch);
1251     return ie;
1252 }
1253
1254 CVSSettings CVSPlugin::settings() const
1255 {
1256     return m_settings;
1257 }
1258
1259 void CVSPlugin::setSettings(const CVSSettings &s)
1260 {
1261     if (s != m_settings) {
1262         m_settings = s;
1263         if (QSettings *settings = Core::ICore::instance()->settings())
1264             m_settings.toSettings(settings);
1265     }
1266 }
1267
1268 CVSPlugin *CVSPlugin::cvsPluginInstance()
1269 {
1270     QTC_ASSERT(m_cvsPluginInstance, return m_cvsPluginInstance);
1271     return m_cvsPluginInstance;
1272 }
1273
1274 bool CVSPlugin::vcsAdd(const QString &workingDir, const QString &rawFileName)
1275 {
1276     QStringList args;
1277     args << QLatin1String("add") << rawFileName;
1278     const CVSResponse response =
1279             runCVS(workingDir, args, m_settings.timeOutMS(),
1280                    SshPasswordPrompt|ShowStdOutInLogWindow);
1281     return response.result == CVSResponse::Ok;
1282 }
1283
1284 bool CVSPlugin::vcsDelete(const QString &workingDir, const QString &rawFileName)
1285 {
1286     QStringList args;
1287     args << QLatin1String("remove") << QLatin1String("-f") << rawFileName;
1288     const CVSResponse response =
1289             runCVS(workingDir, args, m_settings.timeOutMS(),
1290                    SshPasswordPrompt|ShowStdOutInLogWindow);
1291     return response.result == CVSResponse::Ok;
1292 }
1293
1294 /* CVS has a "CVS" directory in each directory it manages. The top level
1295  * is the first directory under the directory that does not have it. */
1296 bool CVSPlugin::managesDirectory(const QString &directory, QString *topLevel /* = 0 */) const
1297 {
1298     if (topLevel)
1299         topLevel->clear();
1300     bool manages = false;
1301     const QDir dir(directory);
1302     do {
1303         if (!dir.exists() || !checkCVSDirectory(dir))
1304             break;
1305         manages = true;
1306         if (!topLevel)
1307             break;
1308         /* Recursing up, the top level is a child of the first directory that does
1309          * not have a  "CVS" directory. The starting directory must be a managed
1310          * one. Go up and try to find the first unmanaged parent dir. */
1311         QDir lastDirectory = dir;
1312         for (QDir parentDir = lastDirectory; parentDir.cdUp() ; lastDirectory = parentDir) {
1313             if (!checkCVSDirectory(parentDir)) {
1314                 *topLevel = lastDirectory.absolutePath();
1315                 break;
1316             }
1317         }
1318     } while (false);
1319     if (CVS::Constants::debug) {
1320         QDebug nsp = qDebug().nospace();
1321         nsp << "CVSPlugin::managesDirectory" << directory << manages;
1322         if (topLevel)
1323             nsp << *topLevel;
1324     }
1325     return manages;
1326 }
1327
1328 bool CVSPlugin::checkCVSDirectory(const QDir &directory) const
1329 {
1330     const QString cvsDir = directory.absoluteFilePath(QLatin1String("CVS"));
1331     return QFileInfo(cvsDir).isDir();
1332 }
1333
1334 CVSControl *CVSPlugin::cvsVersionControl() const
1335 {
1336     return static_cast<CVSControl *>(versionControl());
1337 }
1338
1339 }
1340 }
1341 Q_EXPORT_PLUGIN(CVS::Internal::CVSPlugin)