File indexing completed on 2024-04-21 05:41:05

0001 /*
0002     SPDX-FileCopyrightText: 2019-2020 Nikolai Krasheninnikov <nkrasheninnikov@yandex.ru>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "svnlogdialog.h"
0008 
0009 #include <QProcess>
0010 #include <QMenu>
0011 #include <QTemporaryFile>
0012 #include <QShortcut>
0013 
0014 #include "svncommands.h"
0015 
0016 namespace {
0017 
0018 // Helper function: safe revert to a revision of a possibly modified file. If file could not be
0019 // reverted (for example, repo is inaccessible) it preserves current file.
0020 bool resetAndRevertFileToRevision(const QString &filePath, ulong revision)
0021 {
0022     QTemporaryFile file;
0023     if (!file.open()) {
0024         return false;
0025     }
0026 
0027     bool preserveFile = true;
0028     QFile copyFile(filePath);
0029     if (!copyFile.open(QIODevice::ReadOnly)) {
0030         preserveFile = false;
0031     } else {
0032         const QByteArray data = copyFile.readAll();
0033         if (file.write(data) != data.size() || !file.flush()) {
0034             preserveFile = false;
0035         }
0036     }
0037 
0038     if (!SvnCommands::revertLocalChanges(filePath)) {
0039         return false;
0040     }
0041     if (!SvnCommands::revertToRevision(filePath, revision)) {
0042         if (preserveFile) {
0043             QFile::remove(filePath);
0044             QFile::copy(file.fileName(), filePath);
0045         }
0046         return false;
0047     }
0048 
0049     return true;
0050 }
0051 
0052 }
0053 
0054 struct svnLogEntryInfo_t {
0055     svnLogEntryInfo_t() :
0056         remotePath(QString()),
0057         localPath(QString()),
0058         revision(0)
0059     {}
0060 
0061     QString remotePath;         ///< Affected remote path.
0062     QString localPath;          ///< Affected local path.
0063     ulong revision;             ///< Revision number.
0064 };
0065 Q_DECLARE_METATYPE(svnLogEntryInfo_t);
0066 
0067 enum columns_t {
0068     columnRevision,
0069     columnAuthor,
0070     columnDate,
0071     columnMessage
0072 };
0073 
0074 SvnLogDialog::SvnLogDialog(const QString& contextDir, QWidget *parent) :
0075     QDialog(parent),
0076     m_contextDir(contextDir),
0077     m_logLength(100)
0078 {
0079     m_ui.setupUi(this);
0080 
0081     /*
0082      * Add actions, establish connections.
0083      */
0084     QObject::connect(m_ui.pbOk, &QPushButton::clicked, this, &QWidget::close);
0085     QObject::connect(m_ui.pbRefresh, &QPushButton::clicked, this, &SvnLogDialog::refreshLog);
0086     QObject::connect(m_ui.pbNext100, &QPushButton::clicked, this, [this] () {
0087         m_logLength += 100;
0088         refreshLog();
0089     } );
0090 
0091     QObject::connect(m_ui.tLog, &QWidget::customContextMenuRequested, this, &SvnLogDialog::showContextMenuLog);
0092     QObject::connect(m_ui.lPaths, &QWidget::customContextMenuRequested, this, &SvnLogDialog::showContextMenuChangesList);
0093 
0094     m_updateToRev = new QAction(i18n("Update to revision"), this);
0095     m_updateToRev->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0096     QObject::connect(m_updateToRev, &QAction::triggered, this, &SvnLogDialog::updateRepoToRevision);
0097 
0098     m_revertToRev = new QAction(i18n("Revert to revision"), this);
0099     m_revertToRev->setIcon(QIcon::fromTheme(QStringLiteral("document-revert")));
0100     QObject::connect(m_revertToRev, &QAction::triggered, this, &SvnLogDialog::revertRepoToRevision);
0101 
0102     m_diffFilePrev = new QAction(i18n("Show changes"), this);
0103     m_diffFilePrev->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
0104     QObject::connect(m_diffFilePrev, &QAction::triggered, this, [this] () {
0105         svnLogEntryInfo_t info = m_diffFilePrev->data().value<svnLogEntryInfo_t>();
0106         Q_EMIT diffBetweenRevs(info.remotePath, info.revision, info.revision - 1);
0107     } );
0108 
0109     m_diffFileCurrent = new QAction(i18n("Changes against working copy"), this);
0110     m_diffFileCurrent->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
0111     QObject::connect(m_diffFileCurrent, &QAction::triggered, this, [this] () {
0112         svnLogEntryInfo_t info = m_diffFileCurrent->data().value<svnLogEntryInfo_t>();
0113         Q_EMIT diffAgainstWorkingCopy(info.localPath, info.revision);
0114     } );
0115 
0116     m_fileRevertToRev = new QAction(i18n("Revert to revision"), this);
0117     m_fileRevertToRev->setIcon(QIcon::fromTheme(QStringLiteral("document-revert")));
0118     QObject::connect(m_fileRevertToRev, &QAction::triggered, this, &SvnLogDialog::revertFileToRevision);
0119 
0120     QShortcut *refreshShortcut = new QShortcut(QKeySequence::Refresh, this);
0121     QObject::connect(refreshShortcut, &QShortcut::activated, this, &SvnLogDialog::refreshLog);
0122     refreshShortcut->setAutoRepeat(false);
0123 
0124     /*
0125      * Additional setup.
0126      */
0127     m_ui.tLog->horizontalHeader()->setSectionResizeMode(columnDate, QHeaderView::ResizeToContents);
0128 
0129     refreshLog();
0130 }
0131 
0132 SvnLogDialog::~SvnLogDialog() = default;
0133 
0134 void SvnLogDialog::setCurrentRevision(ulong revision)
0135 {
0136     if (m_log.isNull()) {
0137         return;
0138     }
0139 
0140     for (int i = 0; i < m_log->size(); ++i) {
0141         if (m_log->at(i).revision == revision) {
0142             QFont font;
0143             font.setBold(true);
0144 
0145             m_ui.tLog->item(i, columnRevision)->setFont(font);
0146             m_ui.tLog->item(i, columnAuthor)->setFont(font);
0147             m_ui.tLog->item(i, columnDate)->setFont(font);
0148             m_ui.tLog->item(i, columnMessage)->setFont(font);
0149 
0150             m_ui.tLog->selectRow(i);
0151 
0152             break;
0153         }
0154     }
0155 }
0156 
0157 void SvnLogDialog::refreshLog()
0158 {
0159     m_log = SvnCommands::getLog(m_contextDir, m_logLength);
0160 
0161     if (m_log.isNull()) {
0162         return;
0163     }
0164 
0165     m_ui.tLog->clearContents();
0166     m_ui.teMessage->clear();
0167     m_ui.lPaths->clear();
0168 
0169     m_ui.tLog->setRowCount( m_log->size() );
0170     for (int i = 0; i < m_log->size(); ++i) {
0171         QTableWidgetItem *revision = new QTableWidgetItem(QString::number(m_log->at(i).revision));
0172         QTableWidgetItem *author = new QTableWidgetItem(m_log->at(i).author);
0173         QTableWidgetItem *date = new QTableWidgetItem(m_log->at(i).date.toString(QStringLiteral("yyyy.MM.dd hh:mm:ss")));
0174         QTableWidgetItem *msg = new QTableWidgetItem(m_log->at(i).msg);
0175 
0176         revision->setData(Qt::UserRole, QVariant::fromValue(m_log->at(i).revision));
0177 
0178         m_ui.tLog->setItem(i, columnRevision, revision);
0179         m_ui.tLog->setItem(i, columnAuthor, author);
0180         m_ui.tLog->setItem(i, columnDate, date);
0181         m_ui.tLog->setItem(i, columnMessage, msg);
0182     }
0183 
0184     SvnLogDialog::setCurrentRevision( SvnCommands::localRevision(m_contextDir) );
0185 }
0186 
0187 void SvnLogDialog::on_tLog_currentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn)
0188 {
0189     Q_UNUSED(currentColumn)
0190     Q_UNUSED(previousRow)
0191     Q_UNUSED(previousColumn)
0192 
0193     if (currentRow < 0) {
0194         return;
0195     }
0196     if (m_log.isNull()) {
0197         return;
0198     }
0199     if (m_log->size() < currentRow) {
0200         return;
0201     }
0202     if (m_log->empty()) {
0203         return;
0204     }
0205 
0206     const QString rootUrl = SvnCommands::remoteRootUrl(m_contextDir);
0207     if (rootUrl.isEmpty()) {
0208         return;
0209     }
0210 
0211     m_ui.teMessage->setPlainText( m_log->at(currentRow).msg );
0212     m_ui.lPaths->clear();
0213 
0214     for (const auto &i : std::as_const(m_log->at(currentRow).affectedPaths)) {
0215         svnLogEntryInfo_t info;
0216         info.remotePath = rootUrl + i.path;
0217         info.localPath = m_contextDir + i.path;
0218         info.revision = m_log->at(currentRow).revision;
0219 
0220         // For user: let's show relative path from 'svn log'.
0221         QListWidgetItem *item = new QListWidgetItem(i.path, m_ui.lPaths);
0222         item->setData(Qt::UserRole, QVariant::fromValue(info));
0223         m_ui.lPaths->addItem(item);
0224     }
0225 }
0226 
0227 void SvnLogDialog::showContextMenuLog(const QPoint &pos)
0228 {
0229     QTableWidgetItem *item = m_ui.tLog->item( m_ui.tLog->currentRow(), columnRevision );
0230     if (item == nullptr) {
0231         return;
0232     }
0233 
0234     m_updateToRev->setData( item->data(Qt::UserRole) );
0235     m_revertToRev->setData( item->data(Qt::UserRole) );
0236 
0237     QMenu *menu = new QMenu(this);
0238     menu->addAction(m_updateToRev);
0239     menu->addAction(m_revertToRev);
0240 
0241     // Adjust popup menu position for QTableWidget header height.
0242     const QPoint popupPoint = QPoint(pos.x(), pos.y() + m_ui.tLog->horizontalHeader()->height());
0243     menu->exec( m_ui.tLog->mapToGlobal(popupPoint) );
0244 }
0245 
0246 void SvnLogDialog::showContextMenuChangesList(const QPoint &pos)
0247 {
0248     QListWidgetItem *item = m_ui.lPaths->currentItem();
0249     if (item == nullptr) {
0250         return;
0251     }
0252     const svnLogEntryInfo_t info = item->data(Qt::UserRole).value<svnLogEntryInfo_t>();
0253 
0254     m_diffFilePrev->setData( QVariant::fromValue(info) );
0255     m_diffFileCurrent->setData( QVariant::fromValue(info) );
0256     m_fileRevertToRev->setData( QVariant::fromValue(info) );
0257 
0258     QMenu *menu = new QMenu(this);
0259     menu->addAction(m_diffFilePrev);
0260     menu->addAction(m_diffFileCurrent);
0261     menu->addAction(m_fileRevertToRev);
0262 
0263     menu->exec( m_ui.lPaths->mapToGlobal(pos) );
0264 }
0265 
0266 void SvnLogDialog::updateRepoToRevision()
0267 {
0268     bool convert = false;
0269     uint revision = m_updateToRev->data().toUInt(&convert);
0270     if (!convert || !SvnCommands::updateToRevision(m_contextDir, revision)) {
0271         Q_EMIT errorMessage(i18nc("@info:status", "SVN log: update to revision failed."));
0272     } else {
0273         Q_EMIT operationCompletedMessage(i18nc("@info:status", "SVN log: update to revision %1 successful.", revision));
0274         SvnLogDialog::refreshLog();
0275     }
0276 }
0277 
0278 void SvnLogDialog::revertRepoToRevision()
0279 {
0280     bool convert = false;
0281     uint revision = m_revertToRev->data().toUInt(&convert);
0282     if (!convert || !SvnCommands::revertToRevision(m_contextDir, revision)) {
0283         Q_EMIT errorMessage(i18nc("@info:status", "SVN log: revert to revision failed."));
0284     } else {
0285         Q_EMIT operationCompletedMessage(i18nc("@info:status", "SVN log: revert to revision %1 successful.", revision));
0286     }
0287 }
0288 
0289 void SvnLogDialog::revertFileToRevision()
0290 {
0291     svnLogEntryInfo_t info = m_fileRevertToRev->data().value<svnLogEntryInfo_t>();
0292 
0293     if (!resetAndRevertFileToRevision(info.localPath, info.revision)) {
0294         Q_EMIT errorMessage(i18nc("@info:status", "SVN log: revert to revision failed."));
0295     } else {
0296         Q_EMIT operationCompletedMessage(i18nc("@info:status", "SVN log: revert to revision %1 successful.", info.revision));
0297     }
0298 }
0299 
0300 #include "moc_svnlogdialog.cpp"