Commit d7de9444 authored by Nikolai Krasheninnikov's avatar Nikolai Krasheninnikov Committed by Elvis Angelaccio
Browse files

SVN: added SVN Log dialog

Summary:
Added SVN Log dialog. Dialog looks and behaves similar to a TortoiseSVN one.
Dialog supports:
- update repo to specified revision;
- revert repo to specified revision;
- revert file to a specified revision;
- show changes against previois commit;
- show changes against working copy.
Everything is done by the context menu.

{F8181378}

Test Plan: Run SVN Log dialog and check update works, revert works, revert file works, show changes and show changes against working copy works.

Reviewers: #vdg, meven, elvisangelaccio

Reviewed By: elvisangelaccio

Subscribers: yurchor, anthonyfieroni

Differential Revision: https://phabricator.kde.org/D28102
parent 865e901a
......@@ -6,8 +6,11 @@ set(fileviewsvnplugin_SRCS
fileviewsvnplugin.cpp
svncommands.cpp
svncommitdialog.cpp
svnlogdialog.cpp
)
ki18n_wrap_ui(fileviewsvnplugin_SRCS svnlogdialog.ui)
kconfig_add_kcfg_files(fileviewsvnplugin_SRCS
fileviewsvnpluginsettings.kcfgc
)
......
......@@ -45,6 +45,7 @@
#include <QHeaderView>
#include "svncommitdialog.h"
#include "svnlogdialog.h"
#include "svncommands.h"
K_PLUGIN_FACTORY(FileViewSvnPluginFactory, registerPlugin<FileViewSvnPlugin>();)
......@@ -59,6 +60,7 @@ FileViewSvnPlugin::FileViewSvnPlugin(QObject* parent, const QList<QVariant>& arg
m_addAction(0),
m_removeAction(0),
m_showUpdatesAction(0),
m_logAction(nullptr),
m_command(),
m_arguments(),
m_errorMsg(),
......@@ -115,6 +117,11 @@ FileViewSvnPlugin::FileViewSvnPlugin(QObject* parent, const QList<QVariant>& arg
connect(this, SIGNAL(setShowUpdatesChecked(bool)),
m_showUpdatesAction, SLOT(setChecked(bool)));
m_logAction = new QAction(this);
m_logAction->setText(xi18nc("@action:inmenu", "SVN Log..."));
connect(m_logAction, &QAction::triggered,
this, &FileViewSvnPlugin::logDialog);
connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &FileViewSvnPlugin::slotOperationCompleted);
connect(&m_process, &QProcess::errorOccurred,
......@@ -370,7 +377,7 @@ void FileViewSvnPlugin::commitDialog()
connect(this, &FileViewSvnPlugin::versionInfoUpdated, svnCommitDialog, &SvnCommitDialog::refreshChangesList);
connect(svnCommitDialog, &SvnCommitDialog::revertFiles, this, QOverload<const QStringList&>::of(&FileViewSvnPlugin::revertFiles));
connect(svnCommitDialog, &SvnCommitDialog::diffFile, this, &FileViewSvnPlugin::diffFile);
connect(svnCommitDialog, &SvnCommitDialog::diffFile, this, QOverload<const QString&>::of(&FileViewSvnPlugin::diffFile));
connect(svnCommitDialog, &SvnCommitDialog::addFiles, this, QOverload<const QStringList&>::of(&FileViewSvnPlugin::addFiles));
connect(svnCommitDialog, &SvnCommitDialog::commit, this, &FileViewSvnPlugin::commitFiles);
......@@ -409,6 +416,19 @@ void FileViewSvnPlugin::revertFiles()
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::slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus)
{
m_pendingOperation = false;
......@@ -462,19 +482,23 @@ void FileViewSvnPlugin::diffFile(const QString& filePath)
// 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);
// 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(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(),
filePath
localFilePath
}
);
if (!started) {
......@@ -483,6 +507,36 @@ void FileViewSvnPlugin::diffFile(const QString& filePath)
}
}
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)) {
......@@ -598,6 +652,7 @@ QList<QAction*> FileViewSvnPlugin::directoryActions(const KFileItem& directory)
actions.append(m_addAction);
actions.append(m_removeAction);
actions.append(m_revertAction);
actions.append(m_logAction);
return actions;
}
......
......@@ -61,6 +61,7 @@ private slots:
void addFiles();
void removeFiles();
void revertFiles();
void logDialog();
void slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus);
void slotOperationError();
......@@ -69,6 +70,8 @@ private slots:
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);
......@@ -104,6 +107,7 @@ private:
QAction* m_removeAction;
QAction* m_revertAction;
QAction* m_showUpdatesAction;
QAction* m_logAction;
QString m_command;
QStringList m_arguments;
......
......@@ -23,7 +23,9 @@
#include <QProcess>
#include <QTextStream>
#include <QTemporaryFile>
#include <QUrl>
#include <QDir>
#include <QXmlStreamReader>
namespace {
......@@ -66,6 +68,41 @@ ulong SVNCommands::localRevision(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 @@ QString SVNCommands::remoteItemUrl(const QString& filePath)
}
}
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, QFileDev
}
}
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<QFileDevice*>(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<QFileDevice*>(file));
}
QSharedPointer< QVector<logEntry> > 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<logEntry> >{};
}
}
auto log = QSharedPointer< QVector<logEntry> >::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 <path> in revision <revision>'.
if (QTextStream(&process).readAll().indexOf(errorCode) != -1) {
return log;
} else {
return QSharedPointer< QVector<logEntry> >{};
}
}
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<logEntry> >{};
}
if (static_cast<uint>(log->size()) >= maxEntries || itemsAppended == 0) {
break;
} else {
rev = log->back().revision - 1;
}
}
return exportLocalFile(filePath, rev, dynamic_cast<QFileDevice*>(file));
return log;
}
......@@ -22,6 +22,7 @@
#define SVNCOMMANDS_H
#include <QString>
#include <QDateTime>
#include <QtGlobal>
#include <Dolphin/KVersionControlPlugin>
......@@ -29,6 +30,29 @@
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<affectedPath> affectedPaths; ///< Affected paths (files or dirs).
QString msg; ///< Commit message.
};
/**
* \brief SVN support functions.
*
......@@ -43,9 +67,21 @@ public:
* \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 @@ public:
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.