diff --git a/svn/CMakeLists.txt b/svn/CMakeLists.txt index 5059018..65f1e95 100644 --- a/svn/CMakeLists.txt +++ b/svn/CMakeLists.txt @@ -1,31 +1,32 @@ project(fileviewsvnplugin) add_definitions(-DTRANSLATION_DOMAIN=\"fileviewsvnplugin\") set(fileviewsvnplugin_SRCS fileviewsvnplugin.cpp svncommands.cpp svncommitdialog.cpp svnlogdialog.cpp + svncheckoutdialog.cpp ) -ki18n_wrap_ui(fileviewsvnplugin_SRCS svnlogdialog.ui) +ki18n_wrap_ui(fileviewsvnplugin_SRCS svnlogdialog.ui svncheckoutdialog.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 fbad6d5..241ac60 100644 --- a/svn/fileviewsvnplugin.cpp +++ b/svn/fileviewsvnplugin.cpp @@ -1,692 +1,716 @@ /*************************************************************************** * 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 "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(xi18nc("@action:inmenu", "SVN Log...")); + 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)) { 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 { - Q_UNUSED(items) + // Only for a single directory. + if (items.count() != 1 || !items.first().isDir()) { + return {}; + } - return {}; + m_contextDir = items.first().localPath(); + + return QList{} << m_checkoutAction; } void FileViewSvnPlugin::updateFiles() { 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() { QStringList arguments; // If we are reverting a directory let's revert everything in it. if (!m_contextDir.isEmpty()) { arguments << QLatin1String("--depth") << QLatin1String("infinity"); } 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) { for (const auto &i : qAsConst(filesPath)) { m_contextItems.append( QUrl::fromLocalFile(i) ); } m_contextDir.clear(); 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) { // 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(); 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/fileviewsvnplugin.h b/svn/fileviewsvnplugin.h index 3cb982a..1e90b4e 100644 --- a/svn/fileviewsvnplugin.h +++ b/svn/fileviewsvnplugin.h @@ -1,132 +1,134 @@ /*************************************************************************** * 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 * ***************************************************************************/ #ifndef FILEVIEWSVNPLUGIN_H #define FILEVIEWSVNPLUGIN_H #include #include #include #include #include /** * @brief Subversion implementation for the KVersionControlPlugin interface. */ class FileViewSvnPlugin : public KVersionControlPlugin { Q_OBJECT public: FileViewSvnPlugin(QObject* parent, const QList& args); ~FileViewSvnPlugin() override; QString fileName() const override; bool beginRetrieval(const QString& directory) override; void endRetrieval() override; ItemVersion itemVersion(const KFileItem& item) const override; QList versionControlActions(const KFileItemList& items) const override; QList outOfVersionControlActions(const KFileItemList& items) const override; signals: /// Invokes m_showUpdatesAction->setChecked(checked) on the UI thread. void setShowUpdatesChecked(bool checked); /** * Is emitted if current SVN directory status got updated. Not necessarily means * it's changed. Emitted right after #endRetrieval(). */ void versionInfoUpdated(); private slots: void updateFiles(); void showLocalChanges(); void commitDialog(); void addFiles(); void removeFiles(); void revertFiles(); void logDialog(); + void checkoutDialog(); void slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus); void slotOperationError(); void slotShowUpdatesToggled(bool checked); void revertFiles(const QStringList& filesPath); void diffFile(const QString& filePath); void diffAgainstWorkingCopy(const QString& localFilePath, ulong rev); void diffBetweenRevs(const QString& remoteFilePath, ulong rev1, ulong rev2); void addFiles(const QStringList& filesPath); void commitFiles(const QStringList& context, const QString& msg); private: /** * Executes the command "svn {svnCommand}" for the files that have been * set by getting the context menu actions (see contextMenuActions()). * @param infoMsg Message that should be shown before the command is executed. * @param errorMsg Message that should be shown if the execution of the command * has been failed. * @param operationCompletedMsg * Message that should be shown if the execution of the command * has been completed successfully. */ void execSvnCommand(const QString& svnCommand, const QStringList& arguments, const QString& infoMsg, const QString& errorMsg, const QString& operationCompletedMsg); void startSvnCommandProcess(); QList directoryActions(const KFileItem &directory) const; /** * Checks #item parent directory (or its parent directory and so on) is unversioned. * @param item Item to check. * @return True item is in unversioned directory, false otherwise. */ bool isInUnversionedDir(const KFileItem& item) const; private: bool m_pendingOperation; QHash m_versionInfoHash; QAction* m_updateAction; QAction* m_showLocalChangesAction; QAction* m_commitAction; QAction* m_addAction; QAction* m_removeAction; QAction* m_revertAction; QAction* m_showUpdatesAction; QAction* m_logAction; + QAction* m_checkoutAction; QString m_command; QStringList m_arguments; QString m_errorMsg; QString m_operationCompletedMsg; mutable QString m_contextDir; mutable KFileItemList m_contextItems; QProcess m_process; QTemporaryFile m_tempFile; }; #endif // FILEVIEWSVNPLUGIN_H diff --git a/svn/svncheckoutdialog.cpp b/svn/svncheckoutdialog.cpp new file mode 100644 index 0000000..de0f1dc --- /dev/null +++ b/svn/svncheckoutdialog.cpp @@ -0,0 +1,125 @@ +/*************************************************************************** + * 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 "svncheckoutdialog.h" + +#include +#include +#include +#include +#include + +#include "svncommands.h" + +namespace{ + +// Helper function: removes trailing slashes. +QString rstrip(const QString &str) +{ + for (int i = str.size() - 1; i >= 0; --i) { + if (str.at(i) != '/') { + return str.left(i + 1); + } + } + + return {}; +} + +// Helper function: check if path is a valid svn repository URL. +// Information about URL prefix at http://svnbook.red-bean.com/en/1.2/svn-book.html#svn.basic.in-action.wc.tbl-1. +bool isValidSvnRepoUrl(const QString &path) +{ + static const QStringList schemes = { "file", "http", "https", "svn", "svn+ssh" }; + + const QUrl url = QUrl::fromUserInput(path); + + return url.isValid() && schemes.contains( url.scheme() ); +} + +} + +SvnCheckoutDialog::SvnCheckoutDialog(const QString& contextDir, QWidget *parent) : + QDialog(parent), + m_dir(contextDir) +{ + m_ui.setupUi(this); + + /* + * Add actions, establish connections. + */ + connect(m_ui.pbCancel, &QPushButton::clicked, this, &QWidget::close); + QAction *pickDirectory = m_ui.leCheckoutDir->addAction(QIcon::fromTheme("folder"), QLineEdit::TrailingPosition); + connect(pickDirectory, &QAction::triggered, this, [this] () { + const QString dir = QFileDialog::getExistingDirectory(this, i18nc("@title:window", "Choose a directory to checkout"), + QString(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (!dir.isEmpty()) { + m_ui.leCheckoutDir->setText(dir); + } + } ); + + /* + * Additional setup. + */ + const QString repoPath = QApplication::clipboard()->text(); + if (isValidSvnRepoUrl(repoPath)) { + m_ui.leRepository->setText(repoPath); + } else { + m_ui.leCheckoutDir->setText(m_dir); + } +} + +SvnCheckoutDialog::~SvnCheckoutDialog() = default; + +void SvnCheckoutDialog::on_leRepository_textChanged(const QString &text) +{ + if (isValidSvnRepoUrl(text)) { + const QString stripped = rstrip(text); + // If URL ends with a 'trunk' this is a branch - lets consider upper folder name as an + // extraction path. So for '.../SomeRepo/trunk/' result would be 'SomeRepo'. + int astart = -1; + if (stripped.endsWith("trunk")) { + astart = -2; + } + const QString suffix = QDir::separator() + stripped.section('/', astart, astart); + + m_ui.leCheckoutDir->setText(m_dir + suffix); + m_ui.pbOk->setEnabled(true); + } else { + m_ui.pbOk->setEnabled(false); + } +} + +void SvnCheckoutDialog::on_pbOk_clicked() +{ + const QString &url = m_ui.leRepository->text(); + const bool omitExternals = m_ui.cbOmitExternals->isChecked(); + const QString &whereto = m_ui.leCheckoutDir->text(); + + emit infoMessage(i18nc("@info:status", "SVN checkout: checkout in process...")); + + if (!SvnCommands::checkoutRepository(url, omitExternals, whereto)) { + emit errorMessage(i18nc("@info:status", "SVN checkout: checkout failed.")); + } else { + emit operationCompletedMessage(i18nc("@info:status", "SVN checkout: checkout successful.")); + } + + close(); +} diff --git a/svn/svncheckoutdialog.h b/svn/svncheckoutdialog.h new file mode 100644 index 0000000..f0ff81e --- /dev/null +++ b/svn/svncheckoutdialog.h @@ -0,0 +1,48 @@ +/*************************************************************************** + * Copyright (C) 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 SVNCHECKOUTDIALOG_H +#define SVNCHECKOUTDIALOG_H + +#include + +#include "ui_svncheckoutdialog.h" + +class SvnCheckoutDialog : public QDialog { + Q_OBJECT +public: + SvnCheckoutDialog(const QString& contextDir, QWidget *parent = nullptr); + virtual ~SvnCheckoutDialog() override; + +public slots: + void on_leRepository_textChanged(const QString &text); + void on_pbOk_clicked(); + +signals: + void infoMessage(const QString& msg); + void errorMessage(const QString& msg); + void operationCompletedMessage(const QString& msg); + +private: + Ui::SvnCheckoutDialog m_ui; + QString m_dir; +}; + +#endif // SVNCHECKOUTDIALOG_H diff --git a/svn/svncheckoutdialog.ui b/svn/svncheckoutdialog.ui new file mode 100644 index 0000000..8a57b55 --- /dev/null +++ b/svn/svncheckoutdialog.ui @@ -0,0 +1,96 @@ + + + SvnCheckoutDialog + + + + 0 + 0 + 340 + 180 + + + + + 0 + 0 + + + + SVN Checkout + + + + + + URL of repository: + + + + + + + + + + Checkout directory: + + + + + + + + + + Omit externals + + + + + + + + + Qt::Horizontal + + + + 148 + 20 + + + + + + + + false + + + OK + + + + .. + + + + + + + Cancel + + + + .. + + + + + + + + + + diff --git a/svn/svncommands.cpp b/svn/svncommands.cpp index 37500ce..d13c249 100644 --- a/svn/svncommands.cpp +++ b/svn/svncommands.cpp @@ -1,405 +1,426 @@ /*************************************************************************** * 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(); } } 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::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 3e89d44..74bb749 100644 --- a/svn/svncommands.h +++ b/svn/svncommands.h @@ -1,166 +1,176 @@ /*************************************************************************** * 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 * 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 updateToRevision(const QString& filePath, ulong revision); /** * Discards all local changes in a \p filePath. \p filePath could be a sigle 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 * directory. It also could be an absolute or relative. * * \return True on success, false either. */ static bool revertToRevision(const QString& filePath, ulong revision); /** * 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