Commit de28b61c authored by Friedemann Kleint's avatar Friedemann Kleint
Browse files

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.
parent 37acb3b8
......@@ -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)
......@@ -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
......
......@@ -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,
......
......@@ -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(),
......
......@@ -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)
......
......@@ -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;
......
......@@ -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>
......@@ -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();
......
......@@ -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;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment