diff --git a/src/plugins/cpaster/cpasterplugin.cpp b/src/plugins/cpaster/cpasterplugin.cpp index 572e3f862178a4ec0a773c2ae10f82a23d86bee5..3f2b1e1bd834cf69aaf284dcaec7e9e6fd9dd916 100644 --- a/src/plugins/cpaster/cpasterplugin.cpp +++ b/src/plugins/cpaster/cpasterplugin.cpp @@ -43,13 +43,18 @@ #include <coreplugin/actionmanager/command.h> #include <coreplugin/coreconstants.h> #include <coreplugin/editormanager/editormanager.h> +#include <coreplugin/mimedatabase.h> #include <coreplugin/icore.h> #include <coreplugin/messagemanager.h> #include <coreplugin/uniqueidmanager.h> +#include <utils/qtcassert.h> #include <texteditor/itexteditor.h> #include <QtCore/QtPlugin> #include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFileInfo> +#include <QtCore/QTemporaryFile> #include <QtGui/QAction> #include <QtGui/QApplication> #include <QtGui/QClipboard> @@ -130,6 +135,16 @@ void CodepasterPlugin::extensionsInitialized() { } +void CodepasterPlugin::shutdown() +{ + // Delete temporary, fetched files + foreach(const QString &fetchedSnippet, m_fetchedSnippets) { + QFile file(fetchedSnippet); + if (file.exists()) + file.remove(); + } +} + void CodepasterPlugin::post() { IEditor* editor = EditorManager::instance()->currentEditor(); @@ -250,18 +265,100 @@ void CodepasterPlugin::finishPost(const QString &link) m_settingsPage->displayOutput()); } +// Extract the characters that can be used for a file name from a title +// "CodePaster.com-34" -> "CodePastercom34". +static inline QString filePrefixFromTitle(const QString &title) +{ + QString rc; + const int titleSize = title.size(); + rc.reserve(titleSize); + for (int i = 0; i < titleSize; i++) + if (title.at(i).isLetterOrNumber()) + rc.append(title.at(i)); + if (rc.isEmpty()) + rc = QLatin1String("qtcreator"); + return rc; +} + +// Return a temp file pattern with extension or not +static inline QString tempFilePattern(const QString &prefix, + const QString &extension = QString()) +{ + // Get directory + QString pattern = QDir::tempPath(); + if (!pattern.endsWith(QDir::separator())) + pattern.append(QDir::separator()); + // Prefix, placeholder, extension + pattern += prefix; + pattern += QLatin1String("_XXXXXX"); + if (!extension.isEmpty()) { + pattern += QLatin1Char('.'); + pattern += extension; + } + return pattern; +} + +typedef QSharedPointer<QTemporaryFile> TemporaryFilePtr; + +// Write an a temporary file. +TemporaryFilePtr writeTemporaryFile(const QString &namePattern, + const QString &contents, + QString *errorMessage) +{ + TemporaryFilePtr tempFile(new QTemporaryFile(namePattern)); + if (!tempFile->open()) { + *errorMessage = QString::fromLatin1("Unable to open temporary file %1").arg(tempFile->errorString()); + return TemporaryFilePtr(); + } + tempFile->write(contents.toUtf8()); + tempFile->close(); + return tempFile; +} + void CodepasterPlugin::finishFetch(const QString &titleDescription, const QString &content, bool error) { - QString title = titleDescription; + Core::MessageManager *messageManager = ICore::instance()->messageManager(); + // Failure? if (error) { - ICore::instance()->messageManager()->printToOutputPane(content, true); - } else { - EditorManager* manager = EditorManager::instance(); - IEditor* editor = manager->openEditorWithContents(Core::Constants::K_DEFAULT_TEXT_EDITOR_ID, &title, content); - manager->activateEditor(editor); + messageManager->printToOutputPane(content, true); + return; + } + // Write the file out and do a mime type detection on the content. Note + // that for the initial detection, there must not be a suffix + // as we want mime type detection to trigger on the content and not on + // higher-prioritized suffixes. + const QString filePrefix = filePrefixFromTitle(titleDescription); + QString errorMessage; + TemporaryFilePtr tempFile = writeTemporaryFile(tempFilePattern(filePrefix), content, &errorMessage); + if (tempFile.isNull()) { + messageManager->printToOutputPane(errorMessage); + return; + } + // If the mime type has a preferred suffix (cpp/h/patch...), use that for + // the temporary file. This is to make it more convenient to "Save as" + // for the user and also to be able to tell a patch or diff in the VCS plugins + // by looking at the file name of FileManager::currentFile() without expensive checking. + if (const Core::MimeType mimeType = Core::ICore::instance()->mimeDatabase()->findByFile(QFileInfo(tempFile->fileName()))) { + const QString preferredSuffix = mimeType.preferredSuffix(); + if (!preferredSuffix.isEmpty()) { + tempFile = writeTemporaryFile(tempFilePattern(filePrefix, preferredSuffix), content, &errorMessage); + if (tempFile.isNull()) { + messageManager->printToOutputPane(errorMessage); + return; + } + } } + // Keep the file and store in list of files to be removed. + tempFile->setAutoRemove(false); + const QString fileName = tempFile->fileName(); + m_fetchedSnippets.push_back(fileName); + // Open editor with title. + Core::IEditor* editor = EditorManager::instance()->openEditor(fileName); + QTC_ASSERT(editor, return) + editor->setDisplayName(titleDescription); + EditorManager::instance()->activateEditor(editor); } Q_EXPORT_PLUGIN(CodepasterPlugin) diff --git a/src/plugins/cpaster/cpasterplugin.h b/src/plugins/cpaster/cpasterplugin.h index 9306813d587b86c83ae3faa710fbb2cfd5edb5b8..98e9b39ff1be55a368e975bd15756a696a6e25e6 100644 --- a/src/plugins/cpaster/cpasterplugin.h +++ b/src/plugins/cpaster/cpasterplugin.h @@ -32,7 +32,7 @@ #include <extensionsystem/iplugin.h> -#include <QtCore/QList> +#include <QtCore/QStringList> QT_BEGIN_NAMESPACE class QAction; @@ -53,8 +53,9 @@ public: CodepasterPlugin(); ~CodepasterPlugin(); - bool initialize(const QStringList &arguments, QString *error_message); - void extensionsInitialized(); + virtual bool initialize(const QStringList &arguments, QString *error_message); + virtual void extensionsInitialized(); + virtual void shutdown(); public slots: void post(); @@ -69,6 +70,7 @@ private: QAction *m_fetchAction; SettingsPage *m_settingsPage; QList<Protocol*> m_protocols; + QStringList m_fetchedSnippets; }; } // namespace CodePaster diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index a2310888c248b97ab2ba0e3ab70b832aa5d59d3c..1a8c9e3b113eeb0ff9a166d6b425d35e273184cb 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -973,6 +973,26 @@ bool GitClient::synchronousCleanList(const QString &workingDirectory, return true; } +bool GitClient::synchronousApplyPatch(const QString &workingDirectory, + const QString &file, QString *errorMessage) +{ + if (Git::Constants::debug) + qDebug() << Q_FUNC_INFO << workingDirectory; + QStringList args; + args << QLatin1String("apply") << QLatin1String("--whitespace=fix") << file; + QByteArray outputText; + QByteArray errorText; + const bool rc = synchronousGit(workingDirectory, args, &outputText, &errorText); + if (rc) { + if (!errorText.isEmpty()) + *errorMessage = tr("There were warnings while applying %1 to %2:\n%3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText)); + } else { + *errorMessage = tr("Unable apply patch %1 to %2: %3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText)); + return false; + } + return true; +} + // Factory function to create an asynchronous command GitCommand *GitClient::createCommand(const QString &workingDirectory, VCSBase::VCSBaseEditor* editor, diff --git a/src/plugins/git/gitclient.h b/src/plugins/git/gitclient.h index c81347e32aa76c7a550abd9868ebc6e5dcaca7df..81803fe8b2e7a7898c2d3281b495e226527f1578 100644 --- a/src/plugins/git/gitclient.h +++ b/src/plugins/git/gitclient.h @@ -106,6 +106,7 @@ public: const QStringList &files = QStringList(), QString *errorMessage = 0); bool synchronousCleanList(const QString &workingDirectory, QStringList *files, QString *errorMessage); + bool synchronousApplyPatch(const QString &workingDirectory, const QString &file, QString *errorMessage); bool synchronousInit(const QString &workingDirectory); bool synchronousCheckoutFiles(const QString &workingDirectory, QStringList files = QStringList(), diff --git a/src/plugins/git/gitplugin.cpp b/src/plugins/git/gitplugin.cpp index ccd876d7cf734163aa74a39ca55baa5ce1c356c8..56ab327b0acbfc6735f76fde20882665a825defa 100644 --- a/src/plugins/git/gitplugin.cpp +++ b/src/plugins/git/gitplugin.cpp @@ -50,6 +50,8 @@ #include <coreplugin/actionmanager/actioncontainer.h> #include <coreplugin/actionmanager/command.h> #include <coreplugin/editormanager/editormanager.h> +#include <coreplugin/editormanager/ieditor.h> +#include <coreplugin/filemanager.h> #include <utils/qtcassert.h> #include <utils/parameteraction.h> @@ -126,6 +128,7 @@ GitPlugin::GitPlugin() : m_undoAction(0), m_redoAction(0), m_menuAction(0), + m_applyCurrentFilePatchAction(0), m_gitClient(0), m_changeSelectionDialog(0), m_submitActionTriggered(false) @@ -386,6 +389,20 @@ bool GitPlugin::initialize(const QStringList &arguments, QString *errorMessage) tr("Log Repository"), QLatin1String("Git.LogRepository"), globalcontext, true, &GitClient::graphLog); + // Apply current file as patch is handled specially. + parameterActionCommand = + createParameterAction(actionManager, gitContainer, + tr("Apply Patch"), tr("Apply \"%1\""), + QLatin1String("Git.ApplyCurrentFilePatch"), + globalcontext, true); + m_applyCurrentFilePatchAction = parameterActionCommand.first; + connect(m_applyCurrentFilePatchAction, SIGNAL(triggered()), this, + SLOT(applyCurrentFilePatch())); + + createRepositoryAction(actionManager, gitContainer, + tr("Apply Patch..."), QLatin1String("Git.ApplyPatch"), + globalcontext, true, SLOT(promptApplyPatch())); + createRepositoryAction(actionManager, gitContainer, tr("Undo Repository Changes"), QLatin1String("Git.UndoRepository"), globalcontext, false, SLOT(undoRepositoryChanges())); @@ -805,6 +822,74 @@ void GitPlugin::cleanRepository(const QString &directory) dialog.exec(); } +// If the file is modified in an editor, make sure it is saved. +static bool ensureFileSaved(const QString &fileName) +{ + const QList<Core::IEditor*> editors = Core::EditorManager::instance()->editorsForFileName(fileName); + if (editors.isEmpty()) + return true; + Core::IFile *file = editors.front()->file(); + if (!file || !file->isModified()) + return true; + Core::FileManager *fm = Core::ICore::instance()->fileManager(); + bool canceled; + QList<Core::IFile *> files; + files << file; + fm->saveModifiedFiles(files, &canceled); + return !canceled; +} + +void GitPlugin::applyCurrentFilePatch() +{ + const VCSBase::VCSBasePluginState state = currentState(); + QTC_ASSERT(state.hasPatchFile() && state.hasTopLevel(), return); + const QString patchFile = state.currentPatchFile(); + if (!ensureFileSaved(patchFile)) + return; + applyPatch(state.topLevel(), patchFile); +} + +void GitPlugin::promptApplyPatch() +{ + const VCSBase::VCSBasePluginState state = currentState(); + QTC_ASSERT(state.hasTopLevel(), return); + applyPatch(state.topLevel(), QString()); +} + +void GitPlugin::applyPatch(const QString &workingDirectory, QString file) +{ + // Ensure user has been notified about pending changes + switch (m_gitClient->ensureStash(workingDirectory)) { + case GitClient::StashUnchanged: + case GitClient::Stashed: + case GitClient::NotStashed: + break; + default: + return; + } + // Prompt for file + if (file.isEmpty()) { + const QString filter = tr("Patches (*.patch *.diff)"); + file = QFileDialog::getOpenFileName(Core::ICore::instance()->mainWindow(), + tr("Choose patch"), + QString(), filter); + if (file.isEmpty()) + return; + } + // Run! + VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance(); + QString errorMessage; + if (m_gitClient->synchronousApplyPatch(workingDirectory, file, &errorMessage)) { + if (errorMessage.isEmpty()) { + outwin->append(tr("Patch %1 successfully applied to %2").arg(file, workingDirectory)); + } else { + outwin->append(errorMessage); + } + } else { + outwin->appendError(errorMessage); + } +} + void GitPlugin::stash() { // Simple stash without prompt, reset repo. @@ -866,6 +951,8 @@ void GitPlugin::updateActions(VCSBase::VCSBasePlugin::ActionState as) const QString fileName = currentState().currentFileName(); foreach (Utils::ParameterAction *fileAction, m_fileActions) fileAction->setParameter(fileName); + // If the current file looks like a patch, offer to apply + m_applyCurrentFilePatchAction->setParameter(currentState().currentPatchFileDisplayName()); const QString projectName = currentState().currentProjectName(); foreach (Utils::ParameterAction *projectAction, m_projectActions) diff --git a/src/plugins/git/gitplugin.h b/src/plugins/git/gitplugin.h index 413f45d3e9f2fb2a4f7fbc7569c76ff4e33c6563..11c691fa509e864108b994afea37f2189c96db94 100644 --- a/src/plugins/git/gitplugin.h +++ b/src/plugins/git/gitplugin.h @@ -115,6 +115,8 @@ private slots: void unstageFile(); void cleanProject(); void cleanRepository(); + void applyCurrentFilePatch(); + void promptApplyPatch(); void gitClientMemberFuncRepositoryAction(); void showCommit(); @@ -170,6 +172,7 @@ private: Core::IEditor *openSubmitEditor(const QString &fileName, const CommitData &cd); void cleanCommitMessageFile(); void cleanRepository(const QString &directory); + void applyPatch(const QString &workingDirectory, QString file = QString()); static GitPlugin *m_instance; Core::ICore *m_core; @@ -187,6 +190,7 @@ private: QVector<Utils::ParameterAction *> m_fileActions; QVector<Utils::ParameterAction *> m_projectActions; QVector<QAction *> m_repositoryActions; + Utils::ParameterAction *m_applyCurrentFilePatchAction; GitClient *m_gitClient; ChangeSelectionDialog *m_changeSelectionDialog; diff --git a/src/plugins/vcsbase/VCSBase.mimetypes.xml b/src/plugins/vcsbase/VCSBase.mimetypes.xml index b5821a75a6105b4ca7b90e80cb4f315ca16dfa35..501171c1eec1c8b9bbe781eec3b2ece08bc221d0 100644 --- a/src/plugins/vcsbase/VCSBase.mimetypes.xml +++ b/src/plugins/vcsbase/VCSBase.mimetypes.xml @@ -4,5 +4,14 @@ <sub-class-of type="text/plain"/> <comment>Differences between files</comment> <glob pattern="*.patch"/> + <glob pattern="*.diff"/> + <!-- Find unified diffs from code pasting utilities by checking for "+++ foo.cpp" + Note that this must have a higher priority than any content + rule for C++ as we want diffs to take preference when looking + at a C++ patch. + --> + <magic priority="10"> + <match value="+++ " type="string" offset="0:10000"/> + </magic> </mime-type> </mime-info> diff --git a/src/plugins/vcsbase/vcsbaseplugin.cpp b/src/plugins/vcsbase/vcsbaseplugin.cpp index bfa7f80ddecd5b7ecdc9db727df1979bb9fadd05..43321a66374c2d91ce5d07d192ad724cc0c5c952 100644 --- a/src/plugins/vcsbase/vcsbaseplugin.cpp +++ b/src/plugins/vcsbase/vcsbaseplugin.cpp @@ -37,6 +37,8 @@ #include <coreplugin/ifile.h> #include <coreplugin/iversioncontrol.h> #include <coreplugin/filemanager.h> +#include <coreplugin/editormanager/editormanager.h> +#include <coreplugin/editormanager/ieditor.h> #include <coreplugin/vcsmanager.h> #include <projectexplorer/projectexplorer.h> #include <projectexplorer/project.h> @@ -45,6 +47,7 @@ #include <QtCore/QDebug> #include <QtCore/QDir> #include <QtCore/QSharedData> +#include <QtCore/QScopedPointer> #include <QtGui/QAction> #include <QtGui/QMessageBox> @@ -62,6 +65,7 @@ namespace Internal { struct State { void clearFile(); + void clearPatchFile(); void clearProject(); inline void clear(); @@ -73,6 +77,9 @@ struct State { QString currentFile; QString currentFileName; + QString currentPatchFile; + QString currentPatchFileDisplayName; + QString currentFileDirectory; QString currentFileTopLevel; @@ -89,6 +96,12 @@ void State::clearFile() currentFileTopLevel.clear(); } +void State::clearPatchFile() +{ + currentPatchFile.clear(); + currentPatchFileDisplayName.clear(); +} + void State::clearProject() { currentProjectPath.clear(); @@ -99,6 +112,7 @@ void State::clearProject() void State::clear() { clearFile(); + clearPatchFile(); clearProject(); } @@ -106,6 +120,7 @@ bool State::equals(const State &rhs) const { return currentFile == rhs.currentFile && currentFileName == rhs.currentFileName + && currentPatchFile == rhs.currentPatchFile && currentFileTopLevel == rhs.currentFileTopLevel && currentProjectPath == rhs.currentProjectPath && currentProjectName == rhs.currentProjectName @@ -168,6 +183,14 @@ StateListener::StateListener(QObject *parent) : this, SLOT(slotStateChanged())); } +static inline QString displayNameOfEditor(const QString &fileName) +{ + const QList<Core::IEditor*> editors = Core::EditorManager::instance()->editorsForFileName(fileName); + if (!editors.isEmpty()) + return editors.front()->displayName(); + return QString(); +} + void StateListener::slotStateChanged() { const ProjectExplorer::ProjectExplorerPlugin *pe = ProjectExplorer::ProjectExplorerPlugin::instance(); @@ -179,16 +202,35 @@ void StateListener::slotStateChanged() // folder? State state; state.currentFile = core->fileManager()->currentFile(); + QScopedPointer<QFileInfo> currentFileInfo; // Instantiate QFileInfo only once if required. if (!state.currentFile.isEmpty()) { - if (state.currentFile.contains(QLatin1Char('#')) || state.currentFile.startsWith(QDir::tempPath())) + const bool isTempFile = state.currentFile.startsWith(QDir::tempPath()); + // Quick check: Does it look like a patch? + const bool isPatch = state.currentFile.endsWith(QLatin1String(".patch")) + || state.currentFile.endsWith(QLatin1String(".diff")); + if (isPatch) { + // Patch: Figure out a name to display. If it is a temp file, it could be + // Codepaster. Use the display name of the editor. + state.currentPatchFile = state.currentFile; + if (isTempFile) + state.currentPatchFileDisplayName = displayNameOfEditor(state.currentPatchFile); + if (state.currentPatchFileDisplayName.isEmpty()) { + currentFileInfo.reset(new QFileInfo(state.currentFile)); + state.currentPatchFileDisplayName = currentFileInfo->fileName(); + } + } + // For actual version control operations on it: + // Do not show temporary files and project folders ('#') + if (isTempFile || state.currentFile.contains(QLatin1Char('#'))) state.currentFile.clear(); } // Get the file and its control. Do not use the file unless we find one Core::IVersionControl *fileControl = 0; if (!state.currentFile.isEmpty()) { - const QFileInfo fi(state.currentFile); - state.currentFileDirectory = fi.absolutePath(); - state.currentFileName = fi.fileName(); + if (currentFileInfo.isNull()) + currentFileInfo.reset(new QFileInfo(state.currentFile)); + state.currentFileDirectory = currentFileInfo->absolutePath(); + state.currentFileName = currentFileInfo->fileName(); fileControl = vcsManager->findVersionControlForDirectory(state.currentFileDirectory, &state.currentFileTopLevel); if (!fileControl) @@ -212,6 +254,8 @@ void StateListener::slotStateChanged() } // Assemble state and emit signal. Core::IVersionControl *vc = state.currentFile.isEmpty() ? projectControl : fileControl; + if (!vc) // Need a repository to patch + state.clearPatchFile(); if (debug) qDebug() << state << (vc ? vc->displayName() : QString(QLatin1String("No version control"))); emit stateChanged(state, vc); @@ -269,6 +313,16 @@ QString VCSBasePluginState::relativeCurrentFile() const return QDir(data->m_state.currentFileTopLevel).relativeFilePath(data->m_state.currentFile); } +QString VCSBasePluginState::currentPatchFile() const +{ + return data->m_state.currentPatchFile; +} + +QString VCSBasePluginState::currentPatchFileDisplayName() const +{ + return data->m_state.currentPatchFileDisplayName; +} + QString VCSBasePluginState::currentProjectPath() const { return data->m_state.currentProjectPath; @@ -333,6 +387,11 @@ bool VCSBasePluginState::hasFile() const return data->m_state.hasFile(); } +bool VCSBasePluginState::hasPatchFile() const +{ + return !data->m_state.currentPatchFile.isEmpty(); +} + bool VCSBasePluginState::hasProject() const { return data->m_state.hasProject(); diff --git a/src/plugins/vcsbase/vcsbaseplugin.h b/src/plugins/vcsbase/vcsbaseplugin.h index 49e75780c980becc45b1a535dcfcdc5a328fec4c..de9e8412923327fd52ad11487140bee7b3bbdd2d 100644 --- a/src/plugins/vcsbase/vcsbaseplugin.h +++ b/src/plugins/vcsbase/vcsbaseplugin.h @@ -68,6 +68,9 @@ class VCSBasePlugin; * according to the new state. This is done centrally to avoid * single plugins repeatedly invoking searches/QFileInfo on files, * etc. + * Independently, there are accessors for current patch files, which return + * a file name if the current file could be a patch file which could be applied + * and a repository exists. * * If current file/project are managed * by different version controls, the project is discarded and only @@ -92,6 +95,7 @@ public: bool isEmpty() const; bool hasFile() const; + bool hasPatchFile() const; bool hasProject() const; bool hasTopLevel() const; @@ -103,6 +107,11 @@ public: // Convenience: Returns file relative to top level. QString relativeCurrentFile() const; + // If the current file looks like a patch and there is a top level, + // it will end up here (for VCS that offer patch functionality). + QString currentPatchFile() const; + QString currentPatchFileDisplayName() const; + // Current project. QString currentProjectPath() const; QString currentProjectName() const;