diff --git a/svn/CMakeLists.txt b/svn/CMakeLists.txt index 65f1e95..0d96292 100644 --- a/svn/CMakeLists.txt +++ b/svn/CMakeLists.txt @@ -1,32 +1,33 @@ project(fileviewsvnplugin) add_definitions(-DTRANSLATION_DOMAIN=\"fileviewsvnplugin\") set(fileviewsvnplugin_SRCS fileviewsvnplugin.cpp svncommands.cpp 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 ) add_library(fileviewsvnplugin MODULE ${fileviewsvnplugin_SRCS}) target_link_libraries(fileviewsvnplugin Qt5::Core Qt5::Widgets KF5::I18n KF5::XmlGui KF5::KIOCore KF5::KIOWidgets DolphinVcs ) install(FILES fileviewsvnplugin.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES fileviewsvnpluginsettings.kcfg DESTINATION ${KCFG_INSTALL_DIR}) install(TARGETS fileviewsvnplugin DESTINATION ${KDE_INSTALL_PLUGINDIR}) diff --git a/svn/fileviewsvnplugin.cpp b/svn/fileviewsvnplugin.cpp index 241ac60..1970f47 100644 --- a/svn/fileviewsvnplugin.cpp +++ b/svn/fileviewsvnplugin.cpp @@ -1,716 +1,747 @@ /*************************************************************************** * Copyright (C) 2009-2011 by Peter Penz * * * * 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 "fileviewsvnplugin.h" #include "fileviewsvnpluginsettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "svncommitdialog.h" #include "svnlogdialog.h" #include "svncheckoutdialog.h" +#include "svnprogressdialog.h" #include "svncommands.h" K_PLUGIN_FACTORY(FileViewSvnPluginFactory, registerPlugin();) FileViewSvnPlugin::FileViewSvnPlugin(QObject* parent, const QList& args) : KVersionControlPlugin(parent), m_pendingOperation(false), m_versionInfoHash(), m_updateAction(nullptr), m_showLocalChangesAction(nullptr), m_commitAction(nullptr), m_addAction(nullptr), m_removeAction(nullptr), m_showUpdatesAction(nullptr), m_logAction(nullptr), m_command(), m_arguments(), m_errorMsg(), m_operationCompletedMsg(), m_contextDir(), m_contextItems(), m_process(), m_tempFile() { Q_UNUSED(args); m_updateAction = new QAction(this); m_updateAction->setIcon(QIcon::fromTheme("view-refresh")); m_updateAction->setText(i18nc("@item:inmenu", "SVN Update")); connect(m_updateAction, &QAction::triggered, this, &FileViewSvnPlugin::updateFiles); m_showLocalChangesAction = new QAction(this); m_showLocalChangesAction->setIcon(QIcon::fromTheme("view-split-left-right")); m_showLocalChangesAction->setText(i18nc("@item:inmenu", "Show Local SVN Changes")); connect(m_showLocalChangesAction, &QAction::triggered, this, &FileViewSvnPlugin::showLocalChanges); m_commitAction = new QAction(this); m_commitAction->setIcon(QIcon::fromTheme("svn-commit")); m_commitAction->setText(i18nc("@item:inmenu", "SVN Commit...")); connect(m_commitAction, &QAction::triggered, this, &FileViewSvnPlugin::commitDialog); m_addAction = new QAction(this); m_addAction->setIcon(QIcon::fromTheme("list-add")); m_addAction->setText(i18nc("@item:inmenu", "SVN Add")); connect(m_addAction, &QAction::triggered, this, QOverload<>::of(&FileViewSvnPlugin::addFiles)); m_removeAction = new QAction(this); m_removeAction->setIcon(QIcon::fromTheme("list-remove")); m_removeAction->setText(i18nc("@item:inmenu", "SVN Delete")); connect(m_removeAction, &QAction::triggered, this, &FileViewSvnPlugin::removeFiles); m_revertAction = new QAction(this); m_revertAction->setIcon(QIcon::fromTheme("document-revert")); m_revertAction->setText(i18nc("@item:inmenu", "SVN Revert")); connect(m_revertAction, &QAction::triggered, this, QOverload<>::of(&FileViewSvnPlugin::revertFiles)); m_showUpdatesAction = new QAction(this); m_showUpdatesAction->setCheckable(true); m_showUpdatesAction->setText(i18nc("@item:inmenu", "Show SVN Updates")); m_showUpdatesAction->setChecked(FileViewSvnPluginSettings::showUpdates()); connect(m_showUpdatesAction, &QAction::toggled, this, &FileViewSvnPlugin::slotShowUpdatesToggled); connect(this, &FileViewSvnPlugin::setShowUpdatesChecked, m_showUpdatesAction, &QAction::setChecked); m_logAction = new QAction(this); m_logAction->setText(i18nc("@action:inmenu", "SVN Log...")); connect(m_logAction, &QAction::triggered, this, &FileViewSvnPlugin::logDialog); m_checkoutAction = new QAction(this); m_checkoutAction->setText(i18nc("@action:inmenu", "SVN Checkout...")); connect(m_checkoutAction, &QAction::triggered, this, &FileViewSvnPlugin::checkoutDialog); connect(&m_process, QOverload::of(&QProcess::finished), this, &FileViewSvnPlugin::slotOperationCompleted); connect(&m_process, &QProcess::errorOccurred, this, &FileViewSvnPlugin::slotOperationError); } FileViewSvnPlugin::~FileViewSvnPlugin() { } QString FileViewSvnPlugin::fileName() const { return QLatin1String(".svn"); } bool FileViewSvnPlugin::beginRetrieval(const QString& directory) { Q_ASSERT(directory.endsWith(QLatin1Char('/'))); // Clear all entries for this directory including the entries // for sub directories 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(); } } QStringList arguments; arguments << QLatin1String("status"); if (FileViewSvnPluginSettings::showUpdates()) { arguments << QLatin1String("--show-updates"); } arguments << QLatin1String("--no-ignore") << directory; QProcess process; process.start(QLatin1String("svn"), arguments); while (process.waitForReadyRead()) { char buffer[1024]; while (process.readLine(buffer, sizeof(buffer)) > 0) { ItemVersion version = NormalVersion; QString filePath(buffer); switch (buffer[0]) { case 'I': case '?': version = UnversionedVersion; break; case 'M': version = LocallyModifiedVersion; break; case 'A': version = AddedVersion; break; case 'D': version = RemovedVersion; break; case 'C': version = ConflictingVersion; break; case '!': version = MissingVersion; break; default: if (filePath.contains('*')) { version = UpdateRequiredVersion; } else if (filePath.contains("W155010")) { version = UnversionedVersion; } break; } // Only values with a different version as 'NormalVersion' // are added to the hash table. If a value is not in the // hash table, it is automatically defined as 'NormalVersion' // (see FileViewSvnPlugin::itemVersion()). if (version != NormalVersion) { int pos = filePath.indexOf('/'); const int length = filePath.length() - pos - 1; filePath = filePath.mid(pos, length); if (!filePath.isEmpty()) { m_versionInfoHash.insert(filePath, version); } } } } if ((process.exitCode() != 0 || process.exitStatus() != QProcess::NormalExit)) { if (FileViewSvnPluginSettings::showUpdates()) { // Network update failed. Unset ShowUpdates option, which triggers a refresh emit infoMessage(i18nc("@info:status", "SVN status update failed. Disabling Option " "\"Show SVN Updates\".")); emit setShowUpdatesChecked(false); // this is no fail, we just try again differently // furthermore returning false shows an error message that would override our info return true; } else { return false; } } return true; } void FileViewSvnPlugin::endRetrieval() { emit versionInfoUpdated(); } KVersionControlPlugin::ItemVersion FileViewSvnPlugin::itemVersion(const KFileItem& item) const { const QString itemUrl = item.localPath(); if (m_versionInfoHash.contains(itemUrl)) { return m_versionInfoHash.value(itemUrl); } // If parent directory is unversioned item itself is unversioned. if (isInUnversionedDir(item)) { return UnversionedVersion; } if (!item.isDir()) { // files that have not been listed by 'svn status' (= m_versionInfoHash) // are under version control per definition // NOTE: svn status does not report files in unversioned paths const QString path = QFileInfo(itemUrl).path(); return m_versionInfoHash.value(path, NormalVersion); } // The item is a directory. Check whether an item listed by 'svn status' (= m_versionInfoHash) // is part of this directory. In this case a local modification should be indicated in the // directory already. const QString itemDir = itemUrl + QDir::separator(); QHash::const_iterator it = m_versionInfoHash.constBegin(); while (it != m_versionInfoHash.constEnd()) { if (it.key().startsWith(itemDir)) { const ItemVersion version = m_versionInfoHash.value(it.key()); if (version == LocallyModifiedVersion || version == AddedVersion || version == RemovedVersion) { return LocallyModifiedVersion; } } ++it; } return NormalVersion; } QList FileViewSvnPlugin::versionControlActions(const KFileItemList& items) const { // Special case: if any item is in unversioned directory we shouldn't add any actions because // we can do nothing with this item. for (const auto &i : items) { if (isInUnversionedDir(i)) { return {}; } } if (items.count() == 1 && items.first().isDir()) { return directoryActions(items.first()); } foreach (const KFileItem& item, items) { m_contextItems.append(item); } m_contextDir.clear(); const bool noPendingOperation = !m_pendingOperation; if (noPendingOperation) { // iterate all items and check the version version to know which // actions can be enabled const int itemsCount = items.count(); int versionedCount = 0; int editingCount = 0; foreach (const KFileItem& item, items) { const ItemVersion version = itemVersion(item); if (version != UnversionedVersion) { ++versionedCount; } switch (version) { case LocallyModifiedVersion: case ConflictingVersion: case AddedVersion: case RemovedVersion: ++editingCount; break; default: break; } } m_commitAction->setEnabled(editingCount > 0); m_addAction->setEnabled(versionedCount == 0); m_revertAction->setEnabled(editingCount == itemsCount); m_removeAction->setEnabled(versionedCount == itemsCount); } else { m_commitAction->setEnabled(false); m_addAction->setEnabled(false); m_revertAction->setEnabled(false); m_removeAction->setEnabled(false); } m_updateAction->setEnabled(noPendingOperation); QList actions; actions.append(m_updateAction); actions.append(m_commitAction); actions.append(m_addAction); actions.append(m_removeAction); actions.append(m_revertAction); actions.append(m_showUpdatesAction); return actions; } QList FileViewSvnPlugin::outOfVersionControlActions(const KFileItemList& items) const { // Only for a single directory. if (items.count() != 1 || !items.first().isDir()) { return {}; } m_contextDir = items.first().localPath(); return QList{} << m_checkoutAction; } 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."), i18nc("@info:status", "Updated SVN repository.")); } void FileViewSvnPlugin::showLocalChanges() { Q_ASSERT(!m_contextDir.isEmpty()); Q_ASSERT(m_contextItems.isEmpty()); // This temporary file will be deleted on Dolphin close. We make an assumption: // when the file gets deleted kompare has already loaded it and no longer needs it. const QString tmpFileNameTemplate = QString("%1/%2.XXXXXX").arg(QDir::tempPath(), QDir(m_contextDir).dirName()); QTemporaryFile *file = new QTemporaryFile(tmpFileNameTemplate, this); if (!file->open()) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes.")); return; } QProcess process; process.setStandardOutputFile(file->fileName()); process.start( QLatin1String("svn"), QStringList { QLatin1String("diff"), QLatin1String("--git"), m_contextDir } ); if (!process.waitForFinished() || process.exitCode() != 0) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes: svn diff failed.")); file->deleteLater(); return; } const bool started = QProcess::startDetached( QLatin1String("kompare"), QStringList { file->fileName() } ); if (!started) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes: could not start kompare.")); file->deleteLater(); } } void FileViewSvnPlugin::commitDialog() { QStringList context; if (!m_contextDir.isEmpty()) { context << m_contextDir; } else { for (const auto &i : m_contextItems) { context << i.localPath(); } } SvnCommitDialog *svnCommitDialog = new SvnCommitDialog(&m_versionInfoHash, context); connect(this, &FileViewSvnPlugin::versionInfoUpdated, svnCommitDialog, &SvnCommitDialog::refreshChangesList); connect(svnCommitDialog, &SvnCommitDialog::revertFiles, this, QOverload::of(&FileViewSvnPlugin::revertFiles)); connect(svnCommitDialog, &SvnCommitDialog::diffFile, this, QOverload::of(&FileViewSvnPlugin::diffFile)); connect(svnCommitDialog, &SvnCommitDialog::addFiles, this, QOverload::of(&FileViewSvnPlugin::addFiles)); connect(svnCommitDialog, &SvnCommitDialog::commit, this, &FileViewSvnPlugin::commitFiles); svnCommitDialog->setAttribute(Qt::WA_DeleteOnClose); svnCommitDialog->show(); } void FileViewSvnPlugin::addFiles() { execSvnCommand(QLatin1String("add"), QStringList(), i18nc("@info:status", "Adding files to SVN repository..."), i18nc("@info:status", "Adding of files to SVN repository failed."), i18nc("@info:status", "Added files to SVN repository.")); } void FileViewSvnPlugin::removeFiles() { execSvnCommand(QLatin1String("remove"), QStringList(), i18nc("@info:status", "Removing files from SVN repository..."), i18nc("@info:status", "Removing of files from SVN repository failed."), i18nc("@info:status", "Removed files from SVN repository.")); } 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."), i18nc("@info:status", "Reverted files from SVN repository.")); } void FileViewSvnPlugin::logDialog() { SvnLogDialog *svnLogDialog = new SvnLogDialog(m_contextDir); connect(svnLogDialog, &SvnLogDialog::errorMessage, this, &FileViewSvnPlugin::errorMessage); connect(svnLogDialog, &SvnLogDialog::operationCompletedMessage, this, &FileViewSvnPlugin::operationCompletedMessage); connect(svnLogDialog, &SvnLogDialog::diffAgainstWorkingCopy, this, &FileViewSvnPlugin::diffAgainstWorkingCopy); connect(svnLogDialog, &SvnLogDialog::diffBetweenRevs, this, &FileViewSvnPlugin::diffBetweenRevs); svnLogDialog->setAttribute(Qt::WA_DeleteOnClose); svnLogDialog->show(); } void FileViewSvnPlugin::checkoutDialog() { SvnCheckoutDialog *svnCheckoutDialog = new SvnCheckoutDialog(m_contextDir); connect(svnCheckoutDialog, &SvnCheckoutDialog::infoMessage, this, &FileViewSvnPlugin::infoMessage); connect(svnCheckoutDialog, &SvnCheckoutDialog::errorMessage, this, &FileViewSvnPlugin::errorMessage); connect(svnCheckoutDialog, &SvnCheckoutDialog::operationCompletedMessage, this, &FileViewSvnPlugin::operationCompletedMessage); svnCheckoutDialog->setAttribute(Qt::WA_DeleteOnClose); svnCheckoutDialog->show(); } void FileViewSvnPlugin::slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus) { m_pendingOperation = false; if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { emit errorMessage(m_errorMsg); } else if (m_contextItems.isEmpty()) { emit operationCompletedMessage(m_operationCompletedMsg); emit itemVersionsChanged(); } else { startSvnCommandProcess(); } } void FileViewSvnPlugin::slotOperationError() { // don't do any operation on other items anymore m_contextItems.clear(); m_pendingOperation = false; emit errorMessage(m_errorMsg); } void FileViewSvnPlugin::slotShowUpdatesToggled(bool checked) { FileViewSvnPluginSettings* settings = FileViewSvnPluginSettings::self(); Q_ASSERT(settings != 0); settings->setShowUpdates(checked); settings->save(); emit itemVersionsChanged(); } 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."), i18nc("@info:status", "File reverted.")); } void FileViewSvnPlugin::diffFile(const QString& filePath) { // For a diff we will export last known file local revision from a remote and compare. We will // not use basic SVN action 'svn diff --extensions -U ' because we should count // lines or set maximum number for this. // With a maximum number (2147483647) 'svn diff' starts to work slowly. diffAgainstWorkingCopy(filePath, SvnCommands::localRevision(filePath)); } void FileViewSvnPlugin::diffAgainstWorkingCopy(const QString& localFilePath, ulong rev) { QTemporaryFile *file = new QTemporaryFile(this); if (!SvnCommands::exportFile(QUrl::fromLocalFile(localFilePath), rev, file)) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes for a file: could not get file.")); file->deleteLater(); return; } const bool started = QProcess::startDetached( QLatin1String("kompare"), QStringList { file->fileName(), localFilePath } ); if (!started) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes: could not start kompare.")); file->deleteLater(); } } void FileViewSvnPlugin::diffBetweenRevs(const QString& remoteFilePath, ulong rev1, ulong rev2) { QTemporaryFile *file1 = new QTemporaryFile(this); QTemporaryFile *file2 = new QTemporaryFile(this); if (!SvnCommands::exportFile(QUrl::fromLocalFile(remoteFilePath), rev1, file1)) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes for a file: could not get file.")); file1->deleteLater(); return; } if (!SvnCommands::exportFile(QUrl::fromLocalFile(remoteFilePath), rev2, file2)) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes for a file: could not get file.")); file1->deleteLater(); file2->deleteLater(); return; } const bool started = QProcess::startDetached( QLatin1String("kompare"), QStringList { file2->fileName(), file1->fileName() } ); if (!started) { emit errorMessage(i18nc("@info:status", "Could not show local SVN changes: could not start kompare.")); file1->deleteLater(); file2->deleteLater(); } } void FileViewSvnPlugin::addFiles(const QStringList& filesPath) { for (const auto &i : qAsConst(filesPath)) { m_contextItems.append( QUrl::fromLocalFile(i) ); } m_contextDir.clear(); addFiles(); } 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 // be destroyed when the version plugin is destructed. if (!m_tempFile.open()) { emit errorMessage(i18nc("@info:status", "Commit of SVN changes failed.")); return; } QTextStream out(&m_tempFile); const QString fileName = m_tempFile.fileName(); out << msg; m_tempFile.close(); QStringList arguments; arguments << context << "-F" << fileName; // Lets clear m_contextDir and m_contextItems variables: we will pass everything in arguments. // This is needed because startSvnCommandProcess() uses only one QString for svn transaction at // a time but we want to commit everything. 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."), i18nc("@info:status", "Committed SVN changes.")); } void FileViewSvnPlugin::execSvnCommand(const QString& svnCommand, const QStringList& arguments, const QString& infoMsg, const QString& errorMsg, const QString& operationCompletedMsg) { emit infoMessage(infoMsg); m_command = svnCommand; m_arguments = arguments; m_errorMsg = errorMsg; m_operationCompletedMsg = operationCompletedMsg; startSvnCommandProcess(); } void FileViewSvnPlugin::startSvnCommandProcess() { Q_ASSERT(m_process.state() == QProcess::NotRunning); m_pendingOperation = true; const QString program(QLatin1String("svn")); QStringList arguments; arguments << m_command << m_arguments; if (!m_contextDir.isEmpty()) { arguments << m_contextDir; m_contextDir.clear(); } else { // If m_contextDir is empty and m_contextItems is empty then all svn arguments are in // m_arguments (for example see commitFiles()). if (!m_contextItems.isEmpty()) { const KFileItem item = m_contextItems.takeLast(); arguments << item.localPath(); // the remaining items of m_contextItems will be executed // after the process has finished (see slotOperationFinished()) } } m_process.start(program, arguments); } QList FileViewSvnPlugin::directoryActions(const KFileItem& directory) const { m_contextDir = directory.localPath(); if (!m_contextDir.endsWith(QLatin1Char('/'))) { m_contextDir += QLatin1Char('/'); } m_contextItems.clear(); // Only enable the SVN actions if no SVN commands are // executed currently (see slotOperationCompleted() and // startSvnCommandProcess()). const bool enabled = !m_pendingOperation; m_updateAction->setEnabled(enabled); const ItemVersion version = itemVersion(directory); m_showLocalChangesAction->setEnabled(enabled && (version != NormalVersion)); m_addAction->setEnabled(enabled && (version == UnversionedVersion)); m_removeAction->setEnabled(enabled && (version == NormalVersion)); if (version == LocallyModifiedVersion || version == AddedVersion || version == RemovedVersion) { m_commitAction->setEnabled(enabled); m_revertAction->setEnabled(enabled); } else { m_commitAction->setEnabled(false); m_revertAction->setEnabled(false); } QList actions; actions.append(m_updateAction); actions.append(m_showLocalChangesAction); actions.append(m_commitAction); actions.append(m_showUpdatesAction); actions.append(m_addAction); actions.append(m_removeAction); actions.append(m_revertAction); actions.append(m_logAction); return actions; } bool FileViewSvnPlugin::isInUnversionedDir(const KFileItem& item) const { const QString itemPath = item.localPath(); for (auto it = m_versionInfoHash.cbegin(); it != m_versionInfoHash.cend(); ++it) { // Add QDir::separator() to m_versionInfoHash entry to ensure this is a directory. if (it.value() == UnversionedVersion && itemPath.startsWith(it.key() + QDir::separator())) { return true; } } return false; } #include "fileviewsvnplugin.moc" diff --git a/svn/svncommands.cpp b/svn/svncommands.cpp index d13c249..f19f537 100644 --- a/svn/svncommands.cpp +++ b/svn/svncommands.cpp @@ -1,426 +1,482 @@ /*************************************************************************** * 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 "svncommands.h" #include #include #include #include #include #include namespace { // Helper function: returns template file name for QTemporaryFile. QString templateFileName(const QString& url, ulong rev) { const QString tmpFileName = url.section('/', -1); return QDir::tempPath() + QString("/%1.r%2.XXXXXX").arg(tmpFileName).arg(rev); } } ulong SvnCommands::localRevision(const QString& filePath) { QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("info"), QStringLiteral("--show-item"), QStringLiteral("last-changed-revision"), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { return 0; } QTextStream stream(&process); ulong revision = 0; stream >> revision; if (stream.status() == QTextStream::Ok) { return revision; } else { return 0; } } ulong SvnCommands::remoteRevision(const QString& filePath) { const QString url = SvnCommands::remoteItemUrl(filePath); if (url.isNull()) { return 0; } QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("info"), QStringLiteral("--show-item"), QStringLiteral("last-changed-revision"), url } ); if (!process.waitForFinished() || process.exitCode() != 0) { return 0; } QTextStream stream(&process); ulong revision = 0; stream >> revision; if (stream.status() == QTextStream::Ok) { return revision; } else { return 0; } } QString SvnCommands::remoteItemUrl(const QString& filePath) { QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("info"), QStringLiteral("--show-item"), QStringLiteral("url"), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { return 0; } QTextStream stream(&process); QString url; stream >> url; if (stream.status() == QTextStream::Ok) { return url; } else { return QString(); } } QString SvnCommands::remoteRootUrl(const QString& filePath) { QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("info"), QStringLiteral("--show-item"), QStringLiteral("repos-root-url"), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { return 0; } QTextStream stream(&process); QString url; stream >> url; if (stream.status() == QTextStream::Ok) { return url; } else { return QString(); } } QString SvnCommands::remoteRelativeUrl(const QString& filePath) { QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("info"), QStringLiteral("--show-item"), QStringLiteral("relative-url"), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { return 0; } QTextStream stream(&process); QString url; stream >> url; if (stream.status() == QTextStream::Ok) { return url; } else { return QString(); } } +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; process.start( QLatin1String("svn"), QStringList { QStringLiteral("update"), QStringLiteral("-r%1").arg(revision), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { return false; } return true; } bool SvnCommands::revertLocalChanges(const QString& filePath) { QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("revert"), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { return false; } else { return true; } } bool SvnCommands::revertToRevision(const QString& filePath, ulong revision) { // TODO: No conflict resolve while merging. ulong currentRevision = SvnCommands::localRevision(filePath); if (currentRevision == 0) { return false; } QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("merge"), QStringLiteral("-r%1:%2").arg(currentRevision).arg(revision), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { return false; } 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()) { return false; } QString remoteUrl; if (path.isLocalFile()) { remoteUrl = remoteItemUrl(path.path()); if (remoteUrl.isEmpty()) { return false; } } else { remoteUrl = path.url(); } if (!file->isOpen() && !file->open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)) { return false; } QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("export"), QStringLiteral("--force"), QStringLiteral("-r%1").arg(rev), remoteUrl, file->fileName() } ); if (!process.waitForFinished() || process.exitCode() != 0) { return false; } else { return true; } } bool SvnCommands::exportFile(const QUrl& path, ulong rev, QTemporaryFile *file) { if (file == nullptr || !path.isValid()) { return false; } file->setFileTemplate( templateFileName(path.fileName(), rev) ); return exportFile(path, rev, dynamic_cast(file)); } QSharedPointer< QVector > SvnCommands::getLog(const QString& filePath, uint maxEntries, ulong fromRevision) { ulong rev = fromRevision; if (rev == 0) { rev = SvnCommands::remoteRevision(filePath); if (rev == 0) { return QSharedPointer< QVector >{}; } } auto log = QSharedPointer< QVector >::create(); while (true) { // We do 'xml' output as it is the most full output and already in a ready-to-parse format. // Max xml svn log is 255 entries. We should do a while here if there is not enough log // entries parsed already. QProcess process; process.start( QLatin1String("svn"), QStringList { QStringLiteral("log"), QStringLiteral("-r%1:0").arg(rev), QStringLiteral("-l %1").arg(maxEntries), QStringLiteral("--verbose"), QStringLiteral("--xml"), filePath } ); if (!process.waitForFinished() || process.exitCode() != 0) { process.setReadChannel( QProcess::StandardError ); // If stderr contains 'E195012' that means repo doesn't exist in the revision range. // It's not an error: let's return everything we've got already. const QLatin1String errorCode("svn: E195012:"); // Error: 'Unable to find repository location for in revision '. if (QTextStream(&process).readAll().indexOf(errorCode) != -1) { return log; } else { return QSharedPointer< QVector >{}; } } QXmlStreamReader xml(&process); int itemsAppended = 0; if (xml.readNextStartElement() && xml.name() == "log") { while (!xml.atEnd() && xml.readNext() != QXmlStreamReader::EndDocument) { if (!xml.isStartElement() || xml.name() != "logentry") { continue; } logEntry entry; entry.revision = xml.attributes().value("revision").toULong(); if (xml.readNextStartElement() && xml.name() == "author") { entry.author = xml.readElementText(); } if (xml.readNextStartElement() && xml.name() == "date") { entry.date = QDateTime::fromString(xml.readElementText(), Qt::ISODateWithMs); } if (xml.readNextStartElement() && xml.name() == "paths") { while (xml.readNextStartElement() && xml.name() == "path") { affectedPath path; path.action = xml.attributes().value("action").toString(); path.propMods = xml.attributes().value("prop-mods").toString() == "true"; path.textMods = xml.attributes().value("text-mods").toString() == "true"; path.kind = xml.attributes().value("kind").toString(); path.path = xml.readElementText(); entry.affectedPaths.push_back(path); } } if (xml.readNextStartElement() && xml.name() == "msg") { entry.msg = xml.readElementText(); } log->append(entry); itemsAppended++; } } if (xml.hasError()) { // SVN log output parsing failed. return QSharedPointer< QVector >{}; } if (static_cast(log->size()) >= maxEntries || itemsAppended == 0) { break; } else { rev = log->back().revision - 1; } } return log; } bool SvnCommands::checkoutRepository(const QString& url, bool ignoreExternals, const QString& whereto) { QStringList params; params.append(QStringLiteral("checkout")); params.append(url); if (ignoreExternals) { params.append(QStringLiteral("--ignore-externals")); } params.append(whereto); QProcess process; process.start(QLatin1String("svn"), params); // Without timeout because it could be expensive time consuming operation. if (!process.waitForFinished(-1) || process.exitCode() != 0) { return false; } else { return true; } } diff --git a/svn/svncommands.h b/svn/svncommands.h index 74bb749..e465d7e 100644 --- a/svn/svncommands.h +++ b/svn/svncommands.h @@ -1,176 +1,194 @@ /*************************************************************************** * 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 SVNCOMMANDS_H #define SVNCOMMANDS_H #include #include #include #include class QTemporaryFile; class QFileDevice; /** * Path information for log entry. */ struct affectedPath { QString action; ///< Action type: "D" for delete, "M" for modified, etc. bool propMods; ///< Property changes by commit. bool textMods; ///< File changes by commit. QString kind; ///< Path type: "file", "dir", etc. QString path; ///< Path itself. }; /** * A single log entry. */ struct logEntry { ulong revision; ///< Revision number. QString author; ///< Commit author. QDateTime date; ///< Commit time and date. QVector affectedPaths; ///< Affected paths (files or dirs). QString msg; ///< Commit message. }; /** * \brief SVN support functions. * * \note All functions are synchronous i.e. blocking. Each of them waits for svn process to finish. */ class SvnCommands { public: /** * Returns file \p filePath local revision. Local revision means last known file revision, not * last SVN repository revision. * * \return Local revision, 0 in case of error. * * \note This function uses only local SVN data without connection to a remote so it's fast. * \sa remoteRevision() */ static ulong localRevision(const QString& filePath); /** * Returns file \p filePath remote revision. Remote revision means last known SVN repository file * revision. This function uses only current SVN data and doesn't connect to a remote. * * \return Local revision, 0 in case of error. * * \note This function uses only local SVN data without connection to a remote so it's fast. * \sa localRevision() */ static ulong remoteRevision(const QString& filePath); /** * For file \p filePath return its full remote repository URL path. * * \return Remote 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 remoteItemUrl(const QString& filePath); /** * From a file \p filePath returns full remote repository URL in which this file located. For * every file in the repository URL is the same, i.e. returns path used for initial 'svn co'. * * \return Remote 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 remoteRootUrl(const QString& filePath); /** * From a file \p filePath returns relative repository URL in which this file located. So, * for example, a root repository file "file.txt" will have relative URL "^/file.txt". * * \return Relative repository URL, 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 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. * * \note This function uses only local SVN data without connection to a remote so it's fast. */ 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 * already be opened or ready to be opened. Freeing resources is up to the caller. * * \return True if export success, false either. * * \note \p file should already be created with \p new. */ static bool exportFile(const QUrl& path, ulong rev, QFileDevice *file); static bool exportFile(const QUrl& path, ulong rev, QTemporaryFile *file); /** * Get SVN log for a following \p filePath (could be a directory or separate file, relative or * absolute paths accepted). Log starts from revision \p fromRevision and goes for \p maxEntries * previous revisions. The default revision (0) means current revision. * * \return Full log, nullptr in case of error. * * \note This function is really time consuming. */ static QSharedPointer< QVector > getLog(const QString& filePath, uint maxEntries = 255, ulong fromRevision = 0); /** * Check out a working copy of repository \p URL (local URL starts with a 'file://') to a local * path \p whereto (could be relative ot absolute). * * \return True if check out success, false either. * * \note This function can be really time consuming. */ static bool checkoutRepository(const QString& url, bool ignoreExternals, const QString& whereto); }; #endif // SVNCOMMANDS_H diff --git a/svn/svnprogressdialog.cpp b/svn/svnprogressdialog.cpp new file mode 100644 index 0000000..1d9a64e --- /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.h b/svn/svnprogressdialog.h new file mode 100644 index 0000000..58bb1cb --- /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.ui b/svn/svnprogressdialog.ui new file mode 100644 index 0000000..2432191 --- /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 + + + + + + + + +