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 }