Index: svn/CMakeLists.txt =================================================================== --- svn/CMakeLists.txt +++ svn/CMakeLists.txt @@ -6,8 +6,11 @@ fileviewsvnplugin.cpp svncommands.cpp svncommitdialog.cpp + svnlogdialog.cpp ) +ki18n_wrap_ui(fileviewsvnplugin_SRCS svnlogdialog.ui) + kconfig_add_kcfg_files(fileviewsvnplugin_SRCS fileviewsvnpluginsettings.kcfgc ) Index: svn/fileviewsvnplugin.h =================================================================== --- svn/fileviewsvnplugin.h +++ svn/fileviewsvnplugin.h @@ -61,6 +61,7 @@ void addFiles(); void removeFiles(); void revertFiles(); + void logDialog(); void slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus); void slotOperationError(); @@ -69,6 +70,8 @@ void revertFiles(const QStringList& filesPath); void diffFile(const QString& filePath); + void diffFile(const QString& filePath, ulong rev); + void diffFile(const QString& filePath, ulong rev, ulong rev_n1); void addFiles(const QStringList& filesPath); void commitFiles(const QStringList& context, const QString& msg); @@ -104,6 +107,7 @@ QAction* m_removeAction; QAction* m_revertAction; QAction* m_showUpdatesAction; + QAction* m_logAction; QString m_command; QStringList m_arguments; Index: svn/fileviewsvnplugin.cpp =================================================================== --- svn/fileviewsvnplugin.cpp +++ svn/fileviewsvnplugin.cpp @@ -45,6 +45,7 @@ #include #include "svncommitdialog.h" +#include "svnlogdialog.h" #include "svncommands.h" K_PLUGIN_FACTORY(FileViewSvnPluginFactory, registerPlugin();) @@ -53,12 +54,13 @@ KVersionControlPlugin(parent), m_pendingOperation(false), m_versionInfoHash(), - m_updateAction(0), - m_showLocalChangesAction(0), - m_commitAction(0), - m_addAction(0), - m_removeAction(0), - m_showUpdatesAction(0), + 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(), @@ -73,47 +75,52 @@ m_updateAction = new QAction(this); m_updateAction->setIcon(QIcon::fromTheme("view-refresh")); m_updateAction->setText(i18nc("@item:inmenu", "SVN Update")); - connect(m_updateAction, SIGNAL(triggered()), - this, SLOT(updateFiles())); + 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, SIGNAL(triggered()), - this, SLOT(showLocalChanges())); + 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, SIGNAL(triggered()), - this, SLOT(commitDialog())); + 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, SIGNAL(triggered()), - this, SLOT(addFiles())); + 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, SIGNAL(triggered()), - this, SLOT(removeFiles())); + 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, SIGNAL(triggered()), - this, SLOT(revertFiles())); + 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, SIGNAL(toggled(bool)), - this, SLOT(slotShowUpdatesToggled(bool))); - connect(this, SIGNAL(setShowUpdatesChecked(bool)), - m_showUpdatesAction, SLOT(setChecked(bool))); + connect(m_showUpdatesAction, &QAction::toggled, + this, &FileViewSvnPlugin::slotShowUpdatesToggled); + connect(this, &FileViewSvnPlugin::setShowUpdatesChecked, + m_showUpdatesAction, &QAction::setChecked); + + m_logAction = new QAction(this); + m_logAction->setText(xi18nd("@action:inmenu", "SVN Log...")); + connect(m_logAction, &QAction::triggered, + this, &FileViewSvnPlugin::logDialog); connect(&m_process, QOverload::of(&QProcess::finished), this, &FileViewSvnPlugin::slotOperationCompleted); @@ -370,7 +377,7 @@ connect(this, &FileViewSvnPlugin::versionInfoUpdated, svnCommitDialog, &SvnCommitDialog::refreshChangesList); connect(svnCommitDialog, &SvnCommitDialog::revertFiles, this, QOverload::of(&FileViewSvnPlugin::revertFiles)); - connect(svnCommitDialog, &SvnCommitDialog::diffFile, this, &FileViewSvnPlugin::diffFile); + 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); @@ -409,6 +416,19 @@ i18nc("@info:status", "Reverted files from SVN repository.")); } +void FileViewSvnPlugin::logDialog() +{ + SvnLogDialog *svnLogDialog = new SvnLogDialog(m_contextDir); + + QObject::connect(svnLogDialog, &SvnLogDialog::errorMessage, this, &FileViewSvnPlugin::errorMessage); + QObject::connect(svnLogDialog, &SvnLogDialog::operationCompletedMessage, this, &FileViewSvnPlugin::operationCompletedMessage); + QObject::connect(svnLogDialog, &SvnLogDialog::diffAgainstWorkingCopy, this, QOverload::of(&FileViewSvnPlugin::diffFile)); + QObject::connect(svnLogDialog, &SvnLogDialog::diffBetweenRevs, this, QOverload::of(&FileViewSvnPlugin::diffFile)); + + svnLogDialog->setAttribute(Qt::WA_DeleteOnClose); + svnLogDialog->show(); +} + void FileViewSvnPlugin::slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus) { m_pendingOperation = false; @@ -462,12 +482,16 @@ // lines or set maximum number for this. // With a maximum number (2147483647) 'svn diff' starts to work slowly. + diffFile(filePath, SvnCommands::localRevision(filePath)); +} + +void FileViewSvnPlugin::diffFile(const QString& filePath, ulong rev) +{ QTemporaryFile *file = new QTemporaryFile(this); - // TODO: Calling a blocking operation: with a slow connection this might take some time. Work - // should be done in a separate thread or process. - if (!SVNCommands::exportLocalFile(filePath, SVNCommands::localRevision(filePath), file)) { + if (!SvnCommands::exportFile(QUrl::fromLocalFile(filePath), 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( @@ -483,6 +507,36 @@ } } +void FileViewSvnPlugin::diffFile(const QString& filePath, ulong rev, ulong rev_n1) +{ + QTemporaryFile *file1 = new QTemporaryFile(this); + QTemporaryFile *file2 = new QTemporaryFile(this); + if (!SvnCommands::exportFile(QUrl::fromLocalFile(filePath), rev, 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(filePath), rev_n1, 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)) { @@ -598,6 +652,7 @@ actions.append(m_addAction); actions.append(m_removeAction); actions.append(m_revertAction); + actions.append(m_logAction); return actions; } Index: svn/svncommands.h =================================================================== --- svn/svncommands.h +++ svn/svncommands.h @@ -22,6 +22,7 @@ #define SVNCOMMANDS_H #include +#include #include #include @@ -29,12 +30,35 @@ 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 { +class SvnCommands { public: /** * Returns file \p filePath local revision. Local revision means last known file revision, not @@ -43,9 +67,21 @@ * \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. * @@ -56,26 +92,75 @@ static QString remoteItemUrl(const QString& filePath); /** - * Export remote URL \p remoteUrl at revision \p rev to a file \p file. File should already be - * opened or ready to be opened. Freeing resources is up to the caller. + * 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 True if export success, false either. + * \return Remote path, empty QString in case of error. * - * \note \p file should already be created with \p new. + * \note This function uses only local SVN data without connection to a remote so it's fast. */ - static bool exportRemoteFile(const QString& remoteUrl, ulong rev, QFileDevice *file); - static bool exportRemoteFile(const QString& remoteUrl, ulong rev, QTemporaryFile *file); + static QString remoteRootUrl(const QString& filePath); /** - * Export local file \p filePath at revision \p rev to a file \p file. File should already be - * opened or ready to be opened. Freeing resources is up to the caller. + * 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 exportLocalFile(const QString& filePath, ulong rev, QFileDevice *file); - static bool exportLocalFile(const QString& filePath, ulong rev, QTemporaryFile *file); + 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, empty QVector in case of error. + * + * \note This function is really time consuming. + */ + static QSharedPointer< QVector > getLog(const QString& filePath, uint maxEntries = 255, ulong fromRevision = 0); }; #endif // SVNCOMMANDS_H Index: svn/svncommands.cpp =================================================================== --- svn/svncommands.cpp +++ svn/svncommands.cpp @@ -23,7 +23,9 @@ #include #include #include +#include #include +#include namespace { @@ -37,7 +39,7 @@ } -ulong SVNCommands::localRevision(const QString& filePath) +ulong SvnCommands::localRevision(const QString& filePath) { QProcess process; @@ -66,7 +68,42 @@ } } -QString SVNCommands::remoteItemUrl(const QString& filePath) +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; @@ -95,25 +132,93 @@ } } -bool SVNCommands::exportRemoteFile(const QString& remoteUrl, ulong rev, QFileDevice *file) +QString SvnCommands::remoteRootUrl(const QString& filePath) { - if (file == nullptr) { - return false; + 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; } - if (!file->isOpen() && !file->open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)) { + + 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("export"), - QStringLiteral("--force"), - QStringLiteral("-r%1").arg(rev), - remoteUrl, - file->fileName() + QStringLiteral("revert"), + filePath } ); @@ -124,42 +229,177 @@ } } -bool SVNCommands::exportRemoteFile(const QString& remoteUrl, ulong rev, QTemporaryFile *file) +bool SvnCommands::revertToRevision(const QString& filePath, ulong revision) { - if (file == nullptr) { + // TODO: No conflict resolve while merging. + + ulong currentRevision = SvnCommands::localRevision(filePath); + if (currentRevision == 0) { return false; } - file->setFileTemplate( templateFileName(remoteUrl, rev) ); + 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 exportRemoteFile(remoteUrl, rev, dynamic_cast(file)); + return true; } -bool SVNCommands::exportLocalFile(const QString& filePath, ulong rev, QFileDevice *file) +bool SvnCommands::exportFile(const QUrl& path, ulong rev, QFileDevice *file) { - if (file == nullptr) { + if (file == nullptr || !path.isValid()) { return false; } - const QString fileUrl = remoteItemUrl(filePath); - if (fileUrl.isEmpty()) { + 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; } - if (!exportRemoteFile(fileUrl, rev, file)) { + 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::exportLocalFile(const QString& filePath, ulong rev, QTemporaryFile *file) +bool SvnCommands::exportFile(const QUrl& path, ulong rev, QTemporaryFile *file) { - if (file == nullptr) { + if (file == nullptr || !path.isValid()) { return false; } - file->setFileTemplate( templateFileName(filePath, rev) ); + 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. So we should 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 exportLocalFile(filePath, rev, dynamic_cast(file)); + return log; } Index: svn/svncommitdialog.cpp =================================================================== --- svn/svncommitdialog.cpp +++ svn/svncommitdialog.cpp @@ -70,11 +70,16 @@ } -struct svnInfo_t { - QString filePath; - KVersionControlPlugin::ItemVersion fileVersion; +struct svnCommitEntryInfo_t { + svnCommitEntryInfo_t() : + localPath(QString()), + fileVersion( KVersionControlPlugin::NormalVersion ) + {} + + QString localPath; ///< Affected local path. + KVersionControlPlugin::ItemVersion fileVersion; ///< File status in terms of KVersionControlPlugin }; -Q_DECLARE_METATYPE(svnInfo_t); +Q_DECLARE_METATYPE(svnCommitEntryInfo_t); enum columns_t { columnPath, @@ -126,21 +131,21 @@ m_actRevertFile = new QAction(i18nc("@item:inmenu", "Revert"), this); m_actRevertFile->setIcon(QIcon::fromTheme("document-revert")); connect(m_actRevertFile, &QAction::triggered, [this] () { - const QString filePath = m_actRevertFile->data().value().filePath; + const QString filePath = m_actRevertFile->data().value().localPath; emit revertFiles(QStringList() << filePath); } ); m_actDiffFile = new QAction(i18nc("@item:inmenu", "Show changes"), this); m_actDiffFile->setIcon(QIcon::fromTheme("view-split-left-right")); connect(m_actDiffFile, &QAction::triggered, [this] () { - const QString filePath = m_actDiffFile->data().value().filePath; + const QString filePath = m_actDiffFile->data().value().localPath; emit diffFile(filePath); } ); m_actAddFile = new QAction(i18nc("@item:inmenu", "Add file"), this); m_actAddFile->setIcon(QIcon::fromTheme("list-add")); connect(m_actAddFile, &QAction::triggered, [this] () { - const QString filePath = m_actAddFile->data().value().filePath; + const QString filePath = m_actAddFile->data().value().localPath; emit addFiles(QStringList() << filePath); } ); @@ -208,7 +213,9 @@ m_changes->setItem(row, columnStatus, status); row++; - svnInfo_t info { it.key(), it.value() }; + svnCommitEntryInfo_t info; + info.localPath = it.key(); + info.fileVersion = it.value(); path->setData(Qt::UserRole, QVariant::fromValue(info)); status->setData(Qt::UserRole, QVariant::fromValue(info)); @@ -272,7 +279,7 @@ m_actDiffFile->setEnabled(false); m_actAddFile->setEnabled(false); - const svnInfo_t info = data.value(); + const svnCommitEntryInfo_t info = data.value(); switch(info.fileVersion) { case KVersionControlPlugin::UnversionedVersion: m_actAddFile->setEnabled(true); Index: svn/svnlogdialog.h =================================================================== --- /dev/null +++ svn/svnlogdialog.h @@ -0,0 +1,60 @@ +/*************************************************************************** + * 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 SVNLOGDIALOG_H +#define SVNLOGDIALOG_H + +#include +#include + +#include "svncommands.h" + +#include "ui_svnlogdialog.h" + +class SvnLogDialog : public QWidget { + Q_OBJECT +public: + SvnLogDialog(const QString& contextDir, QWidget *parent = nullptr); + virtual ~SvnLogDialog() override; + +public slots: + void setCurrentRevision(ulong revision); + void refreshLog(); + void on_tLog_currentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn); + +signals: + void errorMessage(const QString& msg); + void operationCompletedMessage(const QString& msg); + void diffAgainstWorkingCopy(const QString& localFilePath, ulong rev); + void diffBetweenRevs(const QString& remoteFilePath, ulong rev, ulong rev_n1); + +private: + Ui::SvnLogDialog m_ui; + QSharedPointer< QVector > m_log; + const QString m_contextDir; + uint m_logLength; + QAction *m_updateToRev; + QAction *m_revertToRev; + QAction *m_diffFilePrev; + QAction *m_diffFileCurrent; + QAction *m_fileRevertToRev; +}; + +#endif // SVNLOGDIALOG_H Index: svn/svnlogdialog.cpp =================================================================== --- /dev/null +++ svn/svnlogdialog.cpp @@ -0,0 +1,285 @@ +/*************************************************************************** + * 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 "svnlogdialog.h" + +#include +#include +#include +#include + +#include "svncommands.h" + +namespace { + +// Helper function: safe revert to a revision of a possibly modified file. If file could not be +// reverted (for example, repo is inaccessible) it preserves current file. +bool resetAndRevertFileToRevision(const QString &filePath, ulong revision) +{ + QTemporaryFile file; + if (!file.open()) { + return false; + } + + bool preserveFile = true; + QFile copyFile(filePath); + if (!copyFile.open(QIODevice::ReadOnly)) { + preserveFile = false; + } else { + const QByteArray data = copyFile.readAll(); + if (file.write(data) != data.size() || !file.flush()) { + preserveFile = false; + } + } + + if (!SvnCommands::revertLocalChanges(filePath)) { + return false; + } + if (!SvnCommands::revertToRevision(filePath, revision)) { + if (preserveFile) { + QFile::remove(filePath); + QFile::copy(file.fileName(), filePath); + } + return false; + } + + return true; +} + +} + +struct svnLogEntryInfo_t { + svnLogEntryInfo_t() : + remotePath(QString()), + localPath(QString()), + revision(0) + {} + + QString remotePath; ///< Affected remote path. + QString localPath; ///< Affected local path. + ulong revision; ///< Revision number. +}; +Q_DECLARE_METATYPE(svnLogEntryInfo_t); + +enum columns_t { + columnRevision, + columnAuthor, + columnDate, + columnMessage +}; + +SvnLogDialog::SvnLogDialog(const QString& contextDir, QWidget *parent) : + QWidget(parent), + m_contextDir(contextDir), + m_logLength(100) +{ + m_ui.setupUi(this); + + /* + * Add actions, establish connections. + */ + QObject::connect(m_ui.pbOk, &QPushButton::clicked, this, &QWidget::close); + QObject::connect(m_ui.pbRefresh, &QPushButton::clicked, this, &SvnLogDialog::refreshLog); + QObject::connect(m_ui.pbNext100, &QPushButton::clicked, this, [this] () { + m_logLength += 100; + refreshLog(); + } ); + + QObject::connect(m_ui.tLog, &QWidget::customContextMenuRequested, this, [this] (const QPoint& pos) { + QTableWidgetItem *item = m_ui.tLog->item( m_ui.tLog->currentRow(), columnRevision ); + if (item == nullptr) { + return; + } + + m_updateToRev->setData( item->data(Qt::UserRole) ); + m_revertToRev->setData( item->data(Qt::UserRole) ); + + QMenu *menu = new QMenu(this); + menu->addAction(m_updateToRev); + menu->addAction(m_revertToRev); + + // Adjust popup menu position for QTableWidget header height. + const QPoint popupPoint = QPoint(pos.x(), pos.y() + m_ui.tLog->horizontalHeader()->height()); + menu->exec( m_ui.tLog->mapToGlobal(popupPoint) ); + } ); + + QObject::connect(m_ui.lPaths, &QWidget::customContextMenuRequested, this, [this] (const QPoint& pos) { + QListWidgetItem *item = m_ui.lPaths->currentItem(); + if (item == nullptr) { + return; + } + const svnLogEntryInfo_t info = item->data(Qt::UserRole).value(); + + m_diffFilePrev->setData( QVariant::fromValue(info) ); + m_diffFileCurrent->setData( QVariant::fromValue(info) ); + m_fileRevertToRev->setData( QVariant::fromValue(info) ); + + QMenu *menu = new QMenu(this); + menu->addAction(m_diffFilePrev); + menu->addAction(m_diffFileCurrent); + menu->addAction(m_fileRevertToRev); + + menu->exec( m_ui.lPaths->mapToGlobal(pos) ); + } ); + + m_updateToRev = new QAction(i18n("Update to revision"), this); + m_updateToRev->setIcon(QIcon::fromTheme("view-refresh")); + QObject::connect(m_updateToRev, &QAction::triggered, [this] () { + bool convert = false; + uint revision = m_updateToRev->data().toUInt(&convert); + if (!convert || !SvnCommands::updateToRevision(m_contextDir, revision)) { + emit errorMessage(i18nc("@info:status", "SVN log: update to revision failed.")); + } else { + emit operationCompletedMessage(i18nc("@info:status", "SVN log: update to revision %1 successful.", revision)); + } + + SvnLogDialog::refreshLog(); + } ); + + m_revertToRev = new QAction(i18n("Revert to revision"), this); + m_revertToRev->setIcon(QIcon::fromTheme("document-revert")); + QObject::connect(m_revertToRev, &QAction::triggered, [this] () { + bool convert = false; + uint revision = m_revertToRev->data().toUInt(&convert); + if (!convert || !SvnCommands::revertToRevision(m_contextDir, revision)) { + emit errorMessage(i18nc("@info:status", "SVN log: revert to revision failed.")); + } else { + emit operationCompletedMessage(i18nc("@info:status", "SVN log: revert to revision %1 successful.", revision)); + } + } ); + + m_diffFilePrev = new QAction(i18n("Show changes"), this); + m_diffFilePrev->setIcon(QIcon::fromTheme("view-split-left-right")); + QObject::connect(m_diffFilePrev, &QAction::triggered, [this] () { + svnLogEntryInfo_t info = m_diffFilePrev->data().value(); + emit diffBetweenRevs(info.remotePath, info.revision, info.revision - 1); + } ); + + m_diffFileCurrent = new QAction(i18n("Changes against working copy"), this); + m_diffFileCurrent->setIcon(QIcon::fromTheme("view-split-left-right")); + QObject::connect(m_diffFileCurrent, &QAction::triggered, [this] () { + svnLogEntryInfo_t info = m_diffFileCurrent->data().value(); + emit diffAgainstWorkingCopy(info.localPath, info.revision); + } ); + + m_fileRevertToRev = new QAction(i18n("Revert to revision"), this); + m_fileRevertToRev->setIcon(QIcon::fromTheme("document-revert")); + QObject::connect(m_fileRevertToRev, &QAction::triggered, [this] () { + svnLogEntryInfo_t info = m_fileRevertToRev->data().value(); + + if (!resetAndRevertFileToRevision(info.localPath, info.revision)) { + emit errorMessage(i18nc("@info:status", "SVN log: revert to revision failed.")); + } else { + emit operationCompletedMessage(i18nc("@info:status", "SVN log: revert to revision %1 successful.", info.revision)); + } + } ); + + QShortcut *refreshShortcut = new QShortcut(QKeySequence::Refresh, this); + QObject::connect(refreshShortcut, &QShortcut::activated, this, &SvnLogDialog::refreshLog); + refreshShortcut->setAutoRepeat(false); + + /* + * Additional setup. + */ + m_ui.tLog->horizontalHeader()->setSectionResizeMode(columnDate, QHeaderView::ResizeToContents); + + refreshLog(); +} + +SvnLogDialog::~SvnLogDialog() = default; + +void SvnLogDialog::setCurrentRevision(ulong revision) +{ + for (int it = 0; it < m_log->size(); ++it) { + if (m_log->at(it).revision == revision) { + QFont font; + font.setBold(true); + + m_ui.tLog->item(it, columnRevision)->setFont(font); + m_ui.tLog->item(it, columnAuthor)->setFont(font); + m_ui.tLog->item(it, columnDate)->setFont(font); + m_ui.tLog->item(it, columnMessage)->setFont(font); + + break; + } + } +} + +void SvnLogDialog::refreshLog() +{ + m_log = SvnCommands::getLog(m_contextDir, m_logLength); + m_ui.tLog->clearContents(); + m_ui.teMessage->clear(); + m_ui.lPaths->clear(); + + m_ui.tLog->setRowCount( m_log->size() ); + for (int it = 0; it < m_log->size(); ++it) { + QTableWidgetItem *revision = new QTableWidgetItem(QString::number(m_log->at(it).revision)); + QTableWidgetItem *author = new QTableWidgetItem(m_log->at(it).author); + QTableWidgetItem *date = new QTableWidgetItem(m_log->at(it).date.toString("yyyy.MM.dd hh:mm:ss")); + QTableWidgetItem *msg = new QTableWidgetItem(m_log->at(it).msg); + + revision->setData(Qt::UserRole, QVariant::fromValue(m_log->at(it).revision)); + + m_ui.tLog->setItem(it, columnRevision, revision); + m_ui.tLog->setItem(it, columnAuthor, author); + m_ui.tLog->setItem(it, columnDate, date); + m_ui.tLog->setItem(it, columnMessage, msg); + } + + SvnLogDialog::setCurrentRevision( SvnCommands::localRevision(m_contextDir) ); +} + +void SvnLogDialog::on_tLog_currentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn) +{ + Q_UNUSED(currentColumn) + Q_UNUSED(previousRow) + Q_UNUSED(previousColumn) + + if (currentRow < 0) { + return; + } + if (m_log->size() < currentRow) { + return; + } + if (m_log->empty()) { + return; + } + + const QString rootUrl = SvnCommands::remoteRootUrl(m_contextDir); + if (rootUrl.isEmpty()) { + return; + } + + m_ui.teMessage->setPlainText( m_log->at(currentRow).msg ); + m_ui.lPaths->clear(); + + for (const auto &i : qAsConst(m_log->at(currentRow).affectedPaths)) { + svnLogEntryInfo_t info; + info.remotePath = rootUrl + i.path; + info.localPath = m_contextDir + i.path; + info.revision = m_log->at(currentRow).revision; + + // For user: let's show relative path from 'svn log'. + QListWidgetItem *item = new QListWidgetItem(i.path, m_ui.lPaths); + item->setData(Qt::UserRole, QVariant::fromValue(info)); + m_ui.lPaths->addItem(item); + } +} Index: svn/svnlogdialog.ui =================================================================== --- /dev/null +++ svn/svnlogdialog.ui @@ -0,0 +1,159 @@ + + + SvnLogDialog + + + + 0 + 0 + 726 + 654 + + + + + 0 + 0 + + + + SVN Log + + + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + 1 + + + true + + + false + + + false + + + + + Revision + + + + + Author + + + + + Date + + + + + Message + + + + + + + + Qt::Horizontal + + + + + + + + + + Qt::Horizontal + + + + + + + Qt::CustomContextMenu + + + + + + + + + Next 100 + + + + + + + + + + Refresh + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 448 + 20 + + + + + + + + + 0 + 0 + + + + OK + + + + + + + + + + + + +