diff --git a/svn/CMakeLists.txt b/svn/CMakeLists.txt --- a/svn/CMakeLists.txt +++ b/svn/CMakeLists.txt @@ -8,9 +8,10 @@ svncommitdialog.cpp svnlogdialog.cpp svncheckoutdialog.cpp + svnprogressdialog.cpp ) -ki18n_wrap_ui(fileviewsvnplugin_SRCS svnlogdialog.ui svncheckoutdialog.ui) +ki18n_wrap_ui(fileviewsvnplugin_SRCS svnlogdialog.ui svncheckoutdialog.ui svnprogressdialog.ui) kconfig_add_kcfg_files(fileviewsvnplugin_SRCS fileviewsvnpluginsettings.kcfgc diff --git a/svn/fileviewsvnplugin.cpp b/svn/fileviewsvnplugin.cpp --- a/svn/fileviewsvnplugin.cpp +++ b/svn/fileviewsvnplugin.cpp @@ -47,6 +47,7 @@ #include "svncommitdialog.h" #include "svnlogdialog.h" #include "svncheckoutdialog.h" +#include "svnprogressdialog.h" #include "svncommands.h" @@ -153,7 +154,9 @@ QMutableHashIterator it(m_versionInfoHash); while (it.hasNext()) { it.next(); - if (it.key().startsWith(directory)) { + // 'svn status' return dirs without trailing slash, so without it we can't remove current + // directory from hash. + if ((it.key() + QLatin1Char('/')).startsWith(directory)) { it.remove(); } } @@ -343,6 +346,9 @@ void FileViewSvnPlugin::updateFiles() { + SvnProgressDialog *progressDialog = new SvnProgressDialog(i18nc("@title:window", "SVN Update"), m_contextDir); + progressDialog->connectToProcess(&m_process); + execSvnCommand(QLatin1String("update"), QStringList(), i18nc("@info:status", "Updating SVN repository..."), i18nc("@info:status", "Update of SVN repository failed."), @@ -434,13 +440,24 @@ void FileViewSvnPlugin::revertFiles() { + if (m_contextDir.isEmpty() && m_contextItems.empty()) { + return; + } + QStringList arguments; + QString root; // If we are reverting a directory let's revert everything in it. if (!m_contextDir.isEmpty()) { arguments << QLatin1String("--depth") << QLatin1String("infinity"); + root = m_contextDir; + } else { + root = SvnCommands::localRoot( m_contextItems.last().localPath() ); } + SvnProgressDialog *progressDialog = new SvnProgressDialog(i18nc("@title:window", "SVN Revert"), root); + progressDialog->connectToProcess(&m_process); + execSvnCommand(QStringLiteral("revert"), arguments, i18nc("@info:status", "Reverting files from SVN repository..."), i18nc("@info:status", "Reverting of files from SVN repository failed."), @@ -507,11 +524,18 @@ void FileViewSvnPlugin::revertFiles(const QStringList& filesPath) { + if (filesPath.empty()) { + return; + } + for (const auto &i : qAsConst(filesPath)) { m_contextItems.append( QUrl::fromLocalFile(i) ); } m_contextDir.clear(); + SvnProgressDialog *progressDialog = new SvnProgressDialog(i18nc("@title:window", "SVN Revert"), SvnCommands::localRoot(filesPath.first())); + progressDialog->connectToProcess(&m_process); + execSvnCommand(QLatin1String("revert"), QStringList() << filesPath, i18nc("@info:status", "Reverting changes to file..."), i18nc("@info:status", "Revert file failed."), @@ -592,6 +616,10 @@ void FileViewSvnPlugin::commitFiles(const QStringList& context, const QString& msg) { + if (context.empty()) { + return; + } + // Write the commit description into a temporary file, so // that it can be read by the command "svn commit -F". The temporary // file must stay alive until slotOperationCompleted() is invoked and will @@ -615,6 +643,9 @@ m_contextDir.clear(); m_contextItems.clear(); + SvnProgressDialog *progressDialog = new SvnProgressDialog(i18nc("@title:window", "SVN Commit"), SvnCommands::localRoot(context.first())); + progressDialog->connectToProcess(&m_process); + execSvnCommand(QLatin1String("commit"), arguments, i18nc("@info:status", "Committing SVN changes..."), i18nc("@info:status", "Commit of SVN changes failed."), diff --git a/svn/svncommands.h b/svn/svncommands.h --- a/svn/svncommands.h +++ b/svn/svncommands.h @@ -112,17 +112,26 @@ static QString remoteRelativeUrl(const QString& filePath); /** - * Updates selected \p filePath to revision \p revision. \p filePath could be a sigle file or a + * From file \p filePath returns full working copy root path this file contains. + * + * \return Full local working copy root path, empty QString in case of error. + * + * \note This function uses only local SVN data without connection to a remote so it's fast. + */ + static QString localRoot(const QString& filePath); + + /** + * Updates selected \p filePath to revision \p revision. \p filePath could be a single file or a * directory. It also could be an absolute or relative. * * \return True on success, false either. * - * \note This function uses only local SVN data without connection to a remote so it's fast. + * \note This function can be really time consuming. */ static bool updateToRevision(const QString& filePath, ulong revision); /** - * Discards all local changes in a \p filePath. \p filePath could be a sigle file or a directory. + * Discards all local changes in a \p filePath. \p filePath could be a single file or a directory. * It also could be an absolute or relative. * * \return True on success, false either. @@ -132,13 +141,22 @@ static bool revertLocalChanges(const QString& filePath); /** - * Reverts selected \p filePath to revision \p revision. \p filePath could be a sigle file or a + * Reverts selected \p filePath to revision \p revision. \p filePath could be a single file or a * directory. It also could be an absolute or relative. * * \return True on success, false either. */ static bool revertToRevision(const QString& filePath, ulong revision); + /** + * Runs 'svn cleanup' on a \p dir to remove write locks, resume unfinished operations, etc. Its + * restores directory state if Subversion client has crushed. + * Also this command could be used to remove unversioned or ignored files. + * + * \return True on success, false either. + */ + static bool cleanup(const QString& dir, bool removeUnversioned = false, bool removeIgnored = false, bool includeExternals = false); + /** * Export URL \p path at revision \p rev to a file \p file. URL could be a remote URL to a file * or directory or path to a local file or directory (both relative or absolute). File should diff --git a/svn/svncommands.cpp b/svn/svncommands.cpp --- a/svn/svncommands.cpp +++ b/svn/svncommands.cpp @@ -190,6 +190,35 @@ } } +QString SvnCommands::localRoot(const QString& filePath) +{ + QProcess process; + + process.start( + QLatin1String("svn"), + QStringList { + QStringLiteral("info"), + QStringLiteral("--show-item"), + QStringLiteral("wc-root"), + filePath + } + ); + + if (!process.waitForFinished() || process.exitCode() != 0) { + return 0; + } + + QTextStream stream(&process); + QString wcroot; + stream >> wcroot; + + if (stream.status() == QTextStream::Ok) { + return wcroot; + } else { + return QString(); + } +} + bool SvnCommands::updateToRevision(const QString& filePath, ulong revision) { QProcess process; @@ -256,6 +285,33 @@ return true; } +bool SvnCommands::cleanup(const QString& dir, bool removeUnversioned, bool removeIgnored, bool includeExternals) +{ + QStringList arguments; + arguments << QStringLiteral("cleanup") << dir; + if (removeUnversioned) { + arguments << QStringLiteral("--remove-unversioned"); + } + if (removeIgnored) { + arguments << QStringLiteral("--remove-ignored"); + } + if (includeExternals) { + arguments << QStringLiteral("--include-externals"); + } + + QProcess process; + process.start( + QLatin1String("svn"), + arguments + ); + + if (!process.waitForFinished() || process.exitCode() != 0) { + return false; + } else { + return true; + } +} + bool SvnCommands::exportFile(const QUrl& path, ulong rev, QFileDevice *file) { if (file == nullptr || !path.isValid()) { diff --git a/svn/svnprogressdialog.h b/svn/svnprogressdialog.h new file mode 100644 --- /dev/null +++ b/svn/svnprogressdialog.h @@ -0,0 +1,82 @@ +/*************************************************************************** + * Copyright (C) 2019-2020 * + * by Nikolai Krasheninnikov * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef SVNPROGRESSDIALOG_H +#define SVNPROGRESSDIALOG_H + +#include + +#include "ui_svnprogressdialog.h" + +class QProcess; + +/** + * \brief Dialog for showing SVN operation process. + * + * This dialog connects to the Subversion process (by \p connectToProcess()) and shows its output. + * User has possibility to terminate the process by pressing cancel button. Normally do not need to + * call \p disconnectFromProcess() as it calls automaticaly on connected process finished() signal. + * + * \note This class can call 'svn cleanup' on a Subversion process dir in case of terminating it if + * a working directory were passed to the constructor. + */ +class SvnProgressDialog : public QDialog { + Q_OBJECT +public: + /** + * \param[in] title Dialog title. + * \param[in] workingDir Directory to call 'svn cleanup' on. Empty for no cleanup. + * \param[in,out] parent Parent widget. + */ + SvnProgressDialog(const QString& title, const QString& workingDir = QString(), QWidget *parent = nullptr); + virtual ~SvnProgressDialog() override; + + /** + * Connects to the process signals, stdout and stderr. + */ + void connectToProcess(QProcess *process); + + /** + * Disconnects from previously connected process, nothing happens either. This function is + * automaticaly called on connected process finished() signal. + */ + void disconnectFromProcess(); + +public slots: + void appendInfoText(const QString& text); + void appendErrorText(const QString& text); + void operationCompeleted(); + + virtual void reject() override; + +private: + Ui::SvnProgressDialog m_ui; + + QMetaObject::Connection m_conCancel; + QMetaObject::Connection m_conCompeted; + QMetaObject::Connection m_conProcessError; + QMetaObject::Connection m_conStdOut; + QMetaObject::Connection m_conStrErr; + + bool m_svnTerminated; + const QString m_workingDir; +}; + +#endif // SVNPROGRESSDIALOG_H diff --git a/svn/svnprogressdialog.cpp b/svn/svnprogressdialog.cpp new file mode 100644 --- /dev/null +++ b/svn/svnprogressdialog.cpp @@ -0,0 +1,132 @@ +/*************************************************************************** + * Copyright (C) 2019-2020 * + * by Nikolai Krasheninnikov * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#include "svnprogressdialog.h" + +#include +#include + +#include "svncommands.h" + +SvnProgressDialog::SvnProgressDialog(const QString& title, const QString& workingDir, QWidget *parent) : + QDialog(parent), + m_svnTerminated(false), + m_workingDir(workingDir) +{ + m_ui.setupUi(this); + + /* + * Add actions, establish connections. + */ + QObject::connect(m_ui.buttonOk, &QPushButton::clicked, this, &QDialog::close); + + /* + * Additional setup. + */ + setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle(title); + show(); + activateWindow(); +} + +SvnProgressDialog::~SvnProgressDialog() +{ + disconnectFromProcess(); +} + +void SvnProgressDialog::connectToProcess(QProcess *process) +{ + disconnectFromProcess(); + + m_svnTerminated = false; + + m_conCancel = connect(m_ui.buttonCancel, &QPushButton::clicked, [this, process] () { + process->terminate(); + m_svnTerminated = true; + } ); + m_conCompeted = connect(process, QOverload::of(&QProcess::finished), this, &SvnProgressDialog::operationCompeleted); + m_conProcessError = connect(process, &QProcess::errorOccurred, [this, process] (QProcess::ProcessError) { + const QString commandLine = process->program() + process->arguments().join(' '); + appendErrorText(i18nc("@info:status", "Error starting: %1", commandLine)); + operationCompeleted(); + } ); + m_conStdOut = connect(process, &QProcess::readyReadStandardOutput, [this, process] () { + appendInfoText( process->readAllStandardOutput() ); + } ); + m_conStrErr = connect(process, &QProcess::readyReadStandardError, [this, process] () { + appendErrorText( process->readAllStandardError() ); + } ); +} + +void SvnProgressDialog::disconnectFromProcess() +{ + QObject::disconnect(m_conCancel); + QObject::disconnect(m_conCompeted); + QObject::disconnect(m_conProcessError); + QObject::disconnect(m_conStdOut); + QObject::disconnect(m_conStrErr); +} + +void SvnProgressDialog::appendInfoText(const QString& text) +{ + const QTextCursor pos = m_ui.texteditMessage->textCursor(); + + m_ui.texteditMessage->moveCursor(QTextCursor::End); + m_ui.texteditMessage->insertPlainText(text); + m_ui.texteditMessage->setTextCursor(pos); +} + +void SvnProgressDialog::appendErrorText(const QString& text) +{ + static const QString htmlBegin = ""; + static const QString htmlEnd = "
"; + + QString message = QString(text).replace('\n', QLatin1String("
")); + // Remove last
as it will be in htmlEnd. + if (message.endsWith(QLatin1String("
"))) { + message.chop(4); + } + + m_ui.texteditMessage->appendHtml(htmlBegin + message + htmlEnd); +} + +void SvnProgressDialog::operationCompeleted() +{ + disconnectFromProcess(); + + if (m_svnTerminated && !m_workingDir.isEmpty()) { + if (!SvnCommands::cleanup(m_workingDir)) { + qDebug() << QString("'svn cleanup' failed for %1").arg(m_workingDir); + } + m_svnTerminated = false; + } + + m_ui.buttonOk->setEnabled(true); + m_ui.buttonCancel->setEnabled(false); +} + +void SvnProgressDialog::reject() +{ + if (m_ui.buttonOk->isEnabled()) { + QDialog::reject(); + } else { + emit m_ui.buttonCancel->clicked(); + } +} diff --git a/svn/svnprogressdialog.ui b/svn/svnprogressdialog.ui new file mode 100644 --- /dev/null +++ b/svn/svnprogressdialog.ui @@ -0,0 +1,78 @@ + + + SvnProgressDialog + + + + 0 + 0 + 521 + 409 + + + + + 0 + 0 + + + + + + + + + + true + + + + + + + Cancel + + + + .. + + + + + + + false + + + OK + + + + .. + + + true + + + true + + + + + + + Qt::Horizontal + + + + 328 + 20 + + + + + + + + +