From de28b61cca3ac3f1f147ad57a9ad69aedf892cb6 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint <Friedemann.Kleint@nokia.com> Date: Thu, 25 Mar 2010 16:23:37 +0100 Subject: [PATCH] VCS[git], CodePaster: Add support for applying patches. Modify CodePaster::fetch to do a mimetype detection on the content, create a filename with the matching extension and open that file. This gives correct syntax highlighting and makes "Save as" more convenient. Keep the file around and delete on exit. Modify patch mimetype with some content detection (higher priority than C++). Add a "current patch file" to the VCSBasePlugin::State. Add "Apply patch" to git with whitespace fix. --- src/plugins/cpaster/cpasterplugin.cpp | 109 ++++++++++++++++++++-- src/plugins/cpaster/cpasterplugin.h | 8 +- src/plugins/git/gitclient.cpp | 20 ++++ src/plugins/git/gitclient.h | 1 + src/plugins/git/gitplugin.cpp | 87 +++++++++++++++++ src/plugins/git/gitplugin.h | 4 + src/plugins/vcsbase/VCSBase.mimetypes.xml | 9 ++ src/plugins/vcsbase/vcsbaseplugin.cpp | 67 ++++++++++++- src/plugins/vcsbase/vcsbaseplugin.h | 9 ++ 9 files changed, 301 insertions(+), 13 deletions(-) diff --git a/src/plugins/cpaster/cpasterplugin.cpp b/src/plugins/cpaster/cpasterplugin.cpp index 572e3f86217..3f2b1e1bd83 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 9306813d587..98e9b39ff1b 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 a2310888c24..1a8c9e3b113 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 c81347e32aa..81803fe8b2e 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 ccd876d7cf7..56ab327b0ac 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 413f45d3e9f..11c691fa509 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 b5821a75a61..501171c1eec 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 bfa7f80ddec..43321a66374 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 49e75780c98..de9e8412923 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; -- GitLab