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

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 "svncommands.h"
0008 
0009 #include <QProcess>
0010 #include <QTextStream>
0011 #include <QTemporaryFile>
0012 #include <QUrl>
0013 #include <QDir>
0014 #include <QXmlStreamReader>
0015 
0016 namespace {
0017 
0018 // Helper function: returns template file name for QTemporaryFile.
0019 QString templateFileName(const QString& url, ulong rev)
0020 {
0021     const QString tmpFileName = url.section(QLatin1Char('/'), -1);
0022 
0023     return QDir::tempPath() + QStringLiteral("/%1.r%2.XXXXXX").arg(tmpFileName).arg(rev);
0024 }
0025 
0026 }
0027 
0028 ulong SvnCommands::localRevision(const QString& filePath)
0029 {
0030     QProcess process;
0031 
0032     process.start(
0033         QLatin1String("svn"),
0034         QStringList {
0035             QStringLiteral("info"),
0036             QStringLiteral("--show-item"),
0037             QStringLiteral("last-changed-revision"),
0038             filePath
0039         }
0040     );
0041 
0042     if (!process.waitForFinished() || process.exitCode() != 0) {
0043         return 0;
0044     }
0045 
0046     QTextStream stream(&process);
0047     ulong revision = 0;
0048     stream >> revision;
0049 
0050     if (stream.status() == QTextStream::Ok) {
0051         return revision;
0052     } else {
0053         return 0;
0054     }
0055 }
0056 
0057 ulong SvnCommands::remoteRevision(const QString& filePath)
0058 {
0059     const QString url = SvnCommands::remoteItemUrl(filePath);
0060 
0061     if (url.isNull()) {
0062         return 0;
0063     }
0064 
0065     QProcess process;
0066 
0067     process.start(
0068         QLatin1String("svn"),
0069         QStringList {
0070             QStringLiteral("info"),
0071             QStringLiteral("--show-item"),
0072             QStringLiteral("last-changed-revision"),
0073             url
0074         }
0075     );
0076 
0077     if (!process.waitForFinished() || process.exitCode() != 0) {
0078         return 0;
0079     }
0080 
0081     QTextStream stream(&process);
0082     ulong revision = 0;
0083     stream >> revision;
0084 
0085     if (stream.status() == QTextStream::Ok) {
0086         return revision;
0087     } else {
0088         return 0;
0089     }
0090 }
0091 
0092 QString SvnCommands::remoteItemUrl(const QString& filePath)
0093 {
0094     QProcess process;
0095 
0096     process.start(
0097         QLatin1String("svn"),
0098         QStringList {
0099             QStringLiteral("info"),
0100             QStringLiteral("--show-item"),
0101             QStringLiteral("url"),
0102             filePath
0103         }
0104     );
0105 
0106     if (!process.waitForFinished() || process.exitCode() != 0) {
0107         return QString();
0108     }
0109 
0110     QTextStream stream(&process);
0111     QString url;
0112     stream >> url;
0113 
0114     if (stream.status() == QTextStream::Ok) {
0115         return url;
0116     } else {
0117         return QString();
0118     }
0119 }
0120 
0121 QString SvnCommands::remoteRootUrl(const QString& filePath)
0122 {
0123     QProcess process;
0124 
0125     process.start(
0126         QLatin1String("svn"),
0127         QStringList {
0128             QStringLiteral("info"),
0129             QStringLiteral("--show-item"),
0130             QStringLiteral("repos-root-url"),
0131             filePath
0132         }
0133     );
0134 
0135     if (!process.waitForFinished() || process.exitCode() != 0) {
0136         return QString();
0137     }
0138 
0139     QTextStream stream(&process);
0140     QString url;
0141     stream >> url;
0142 
0143     if (stream.status() == QTextStream::Ok) {
0144         return url;
0145     } else {
0146         return QString();
0147     }
0148 }
0149 
0150 QString SvnCommands::remoteRelativeUrl(const QString& filePath)
0151 {
0152     QProcess process;
0153 
0154     process.start(
0155         QLatin1String("svn"),
0156         QStringList {
0157             QStringLiteral("info"),
0158             QStringLiteral("--show-item"),
0159             QStringLiteral("relative-url"),
0160             filePath
0161         }
0162     );
0163 
0164     if (!process.waitForFinished() || process.exitCode() != 0) {
0165         return QString();
0166     }
0167 
0168     QTextStream stream(&process);
0169     QString url;
0170     stream >> url;
0171 
0172     if (stream.status() == QTextStream::Ok) {
0173         return url;
0174     } else {
0175         return QString();
0176     }
0177 }
0178 
0179 QString SvnCommands::localRoot(const QString& filePath)
0180 {
0181     QProcess process;
0182 
0183     process.start(
0184         QLatin1String("svn"),
0185         QStringList {
0186             QStringLiteral("info"),
0187             QStringLiteral("--show-item"),
0188             QStringLiteral("wc-root"),
0189             filePath
0190         }
0191     );
0192 
0193     if (!process.waitForFinished() || process.exitCode() != 0) {
0194         return QString();
0195     }
0196 
0197     QTextStream stream(&process);
0198     QString wcroot;
0199     stream >> wcroot;
0200 
0201     if (stream.status() == QTextStream::Ok) {
0202         return wcroot;
0203     } else {
0204         return QString();
0205     }
0206 }
0207 
0208 bool SvnCommands::updateToRevision(const QString& filePath, ulong revision)
0209 {
0210     QProcess process;
0211 
0212     process.start(
0213         QLatin1String("svn"),
0214         QStringList {
0215             QStringLiteral("update"),
0216             QStringLiteral("-r%1").arg(revision),
0217             filePath
0218         }
0219     );
0220 
0221     if (!process.waitForFinished() || process.exitCode() != 0) {
0222         return false;
0223     }
0224 
0225     return true;
0226 }
0227 
0228 bool SvnCommands::revertLocalChanges(const QString& filePath)
0229 {
0230     QProcess process;
0231 
0232     process.start(
0233         QLatin1String("svn"),
0234         QStringList {
0235             QStringLiteral("revert"),
0236             filePath
0237         }
0238     );
0239 
0240     if (!process.waitForFinished() || process.exitCode() != 0) {
0241         return false;
0242     } else {
0243         return true;
0244     }
0245 }
0246 
0247 bool SvnCommands::revertToRevision(const QString& filePath, ulong revision)
0248 {
0249     // TODO: No conflict resolve while merging.
0250 
0251     ulong currentRevision = SvnCommands::localRevision(filePath);
0252     if (currentRevision == 0) {
0253         return false;
0254     }
0255 
0256     QProcess process;
0257 
0258     process.start(
0259         QLatin1String("svn"),
0260         QStringList {
0261             QStringLiteral("merge"),
0262             QStringLiteral("-r%1:%2").arg(currentRevision).arg(revision),
0263             filePath
0264         }
0265     );
0266 
0267     if (!process.waitForFinished() || process.exitCode() != 0) {
0268         return false;
0269     }
0270 
0271     return true;
0272 }
0273 
0274 CommandResult SvnCommands::cleanup(const QString& dir, bool removeUnversioned, bool removeIgnored, bool includeExternals)
0275 {
0276     QStringList arguments;
0277     arguments << QStringLiteral("cleanup") << dir;
0278     if (removeUnversioned) {
0279         arguments << QStringLiteral("--remove-unversioned");
0280     }
0281     if (removeIgnored) {
0282         arguments << QStringLiteral("--remove-ignored");
0283     }
0284     if (includeExternals) {
0285         arguments << QStringLiteral("--include-externals");
0286     }
0287 
0288     QProcess process;
0289     process.start(
0290         QLatin1String("svn"),
0291         arguments
0292     );
0293 
0294     CommandResult result;
0295     if (!process.waitForFinished() || process.exitCode() != 0) {
0296         result.success = false;
0297     } else {
0298         result.success = true;
0299     }
0300     result.stdOut = QString::fromLocal8Bit(process.readAllStandardOutput());
0301     result.stdErr = QString::fromLocal8Bit(process.readAllStandardError());
0302 
0303     return result;
0304 }
0305 
0306 bool SvnCommands::exportFile(const QUrl& path, ulong rev, QFileDevice *file)
0307 {
0308     if (file == nullptr || !path.isValid()) {
0309         return false;
0310     }
0311 
0312     QString remoteUrl;
0313     if (path.isLocalFile()) {
0314         remoteUrl = remoteItemUrl(path.path());
0315         if (remoteUrl.isEmpty()) {
0316             return false;
0317         }
0318     } else {
0319         remoteUrl = path.url();
0320     }
0321 
0322     if (!file->isOpen() && !file->open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)) {
0323         return false;
0324     }
0325 
0326     QProcess process;
0327 
0328     process.start(
0329         QLatin1String("svn"),
0330         QStringList {
0331             QStringLiteral("export"),
0332             QStringLiteral("--force"),
0333             QStringLiteral("-r%1").arg(rev),
0334             remoteUrl,
0335             file->fileName()
0336         }
0337     );
0338 
0339     if (!process.waitForFinished() || process.exitCode() != 0) {
0340         return false;
0341     } else {
0342         return true;
0343     }
0344 }
0345 
0346 bool SvnCommands::exportFile(const QUrl& path, ulong rev, QTemporaryFile *file)
0347 {
0348     if (file == nullptr || !path.isValid()) {
0349         return false;
0350     }
0351 
0352     file->setFileTemplate( templateFileName(path.fileName(), rev) );
0353 
0354     return exportFile(path, rev, dynamic_cast<QFileDevice*>(file));
0355 }
0356 
0357 QSharedPointer< QVector<logEntry> > SvnCommands::getLog(const QString& filePath, uint maxEntries, ulong fromRevision)
0358 {
0359     ulong rev = fromRevision;
0360     if (rev == 0) {
0361         rev = SvnCommands::remoteRevision(filePath);
0362         if (rev == 0) {
0363             return QSharedPointer< QVector<logEntry> >{};
0364         }
0365     }
0366 
0367     auto log = QSharedPointer< QVector<logEntry> >::create();
0368     while (true) {
0369         // We do 'xml' output as it is the most full output and already in a ready-to-parse format.
0370         // Max xml svn log is 255 entries. We should do a while here if there is not enough log
0371         // entries parsed already.
0372         QProcess process;
0373         process.start(
0374             QLatin1String("svn"),
0375             QStringList {
0376                 QStringLiteral("log"),
0377                 QStringLiteral("-r%1:0").arg(rev),
0378                 QStringLiteral("-l %1").arg(maxEntries),
0379                 QStringLiteral("--verbose"),
0380                 QStringLiteral("--xml"),
0381                 filePath
0382             }
0383         );
0384 
0385         if (!process.waitForFinished() || process.exitCode() != 0) {
0386             process.setReadChannel( QProcess::StandardError );
0387 
0388             // If stderr contains 'E195012' that means repo doesn't exist in the revision range.
0389             // It's not an error: let's return everything we've got already.
0390             const QLatin1String errorCode("svn: E195012:"); // Error: 'Unable to find repository location for <path> in revision <revision>'.
0391             if (QTextStream(&process).readAll().indexOf(errorCode) != -1) {
0392                 return log;
0393             } else {
0394                 return QSharedPointer< QVector<logEntry> >{};
0395             }
0396         }
0397 
0398         QXmlStreamReader xml(&process);
0399         int itemsAppended = 0;
0400         if (xml.readNextStartElement() && xml.name() == QLatin1String("log")) {
0401             while (!xml.atEnd() && xml.readNext() != QXmlStreamReader::EndDocument) {
0402                 if (!xml.isStartElement() || xml.name() != QLatin1String("logentry")) {
0403                     continue;
0404                 }
0405 
0406                 logEntry entry;
0407                 entry.revision = xml.attributes().value(QLatin1String("revision")).toULong();
0408 
0409                 if (xml.readNextStartElement() && xml.name() == QLatin1String("author")) {
0410                     entry.author = xml.readElementText();
0411                 }
0412                 if (xml.readNextStartElement() && xml.name() == QLatin1String("date")) {
0413                     entry.date = QDateTime::fromString(xml.readElementText(), Qt::ISODateWithMs);
0414                 }
0415 
0416                 if (xml.readNextStartElement() && xml.name() == QLatin1String("paths")) {
0417                     while (xml.readNextStartElement() && xml.name() == QLatin1String("path")) {
0418                         affectedPath path;
0419 
0420                         path.action = xml.attributes().value(QLatin1String("action")).toString();
0421                         path.propMods = xml.attributes().value(QLatin1String("prop-mods")).toString() == QLatin1String("true");
0422                         path.textMods = xml.attributes().value(QLatin1String("text-mods")).toString() == QLatin1String("true");
0423                         path.kind = xml.attributes().value(QLatin1String("kind")).toString();
0424 
0425                         path.path = xml.readElementText();
0426 
0427                         entry.affectedPaths.push_back(path);
0428                     }
0429                 }
0430 
0431                 if (xml.readNextStartElement() && xml.name() == QLatin1String("msg")) {
0432                     entry.msg = xml.readElementText();
0433                 }
0434 
0435                 log->append(entry);
0436                 itemsAppended++;
0437             }
0438         }
0439         if (xml.hasError()) {
0440             // SVN log output parsing failed.
0441             return QSharedPointer< QVector<logEntry> >{};
0442         }
0443 
0444         if (static_cast<uint>(log->size()) >= maxEntries || itemsAppended == 0) {
0445             break;
0446         } else {
0447             rev = log->back().revision - 1;
0448         }
0449     }
0450 
0451     return log;
0452 }
0453 
0454 bool SvnCommands::checkoutRepository(const QString& url, bool ignoreExternals, const QString& whereto)
0455 {
0456     QStringList params;
0457     params.append(QStringLiteral("checkout"));
0458     params.append(url);
0459     if (ignoreExternals) {
0460         params.append(QStringLiteral("--ignore-externals"));
0461     }
0462     params.append(whereto);
0463 
0464     QProcess process;
0465     process.start(QLatin1String("svn"), params);
0466 
0467     // Without timeout because it could be expensive time consuming operation.
0468     if (!process.waitForFinished(-1) || process.exitCode() != 0) {
0469         return false;
0470     } else {
0471         return true;
0472     }
0473 }