File indexing completed on 2024-04-21 05:40:57
0001 /* 0002 SPDX-FileCopyrightText: 2009-2010 Peter Penz <peter.penz@gmx.at> 0003 SPDX-FileCopyrightText: 2011 Canonical Ltd. 0004 SPDX-FileContributor: Jonathan Riddell <jriddell@ubuntu.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "fileviewbazaarplugin.h" 0010 0011 #include <KLocalizedString> 0012 #include <KPluginFactory> 0013 0014 #include <QAction> 0015 #include <QDir> 0016 #include <QLabel> 0017 #include <QPlainTextEdit> 0018 #include <QProcess> 0019 #include <QString> 0020 #include <QStringList> 0021 #include <QTextStream> 0022 0023 K_PLUGIN_CLASS_WITH_JSON(FileViewBazaarPlugin, "fileviewbazaarplugin.json") 0024 0025 FileViewBazaarPlugin::FileViewBazaarPlugin(QObject* parent, const QList<QVariant>& args) : 0026 KVersionControlPlugin(parent), 0027 m_pendingOperation(false), 0028 m_versionInfoHash(), 0029 m_updateAction(nullptr), 0030 m_pullAction(nullptr), 0031 m_pushAction(nullptr), 0032 m_showLocalChangesAction(nullptr), 0033 m_commitAction(nullptr), 0034 m_addAction(nullptr), 0035 m_removeAction(nullptr), 0036 m_logAction(nullptr), 0037 m_command(), 0038 m_arguments(), 0039 m_errorMsg(), 0040 m_operationCompletedMsg(), 0041 m_contextDir(), 0042 m_contextItems(), 0043 m_process(), 0044 m_tempFile() 0045 { 0046 Q_UNUSED(args); 0047 0048 m_updateAction = new QAction(this); 0049 m_updateAction->setText(i18nc("@item:inmenu", "Bazaar Update")); 0050 connect(m_updateAction, &QAction::triggered, 0051 this, &FileViewBazaarPlugin::updateFiles); 0052 0053 m_pullAction = new QAction(this); 0054 m_pullAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-pull"))); 0055 m_pullAction->setText(i18nc("@item:inmenu", "Bazaar Pull")); 0056 connect(m_pullAction, &QAction::triggered, 0057 this, &FileViewBazaarPlugin::pullFiles); 0058 0059 m_pushAction = new QAction(this); 0060 m_pushAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-push"))); 0061 m_pushAction->setText(i18nc("@item:inmenu", "Bazaar Push")); 0062 connect(m_pushAction, &QAction::triggered, 0063 this, &FileViewBazaarPlugin::pushFiles); 0064 0065 m_showLocalChangesAction = new QAction(this); 0066 m_showLocalChangesAction->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right"))); 0067 m_showLocalChangesAction->setText(i18nc("@item:inmenu", "Show Local Bazaar Changes")); 0068 connect(m_showLocalChangesAction, &QAction::triggered, 0069 this, &FileViewBazaarPlugin::showLocalChanges); 0070 0071 m_commitAction = new QAction(this); 0072 m_commitAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-commit"))); 0073 m_commitAction->setText(i18nc("@item:inmenu", "Bazaar Commit...")); 0074 connect(m_commitAction, &QAction::triggered, 0075 this, &FileViewBazaarPlugin::commitFiles); 0076 0077 m_addAction = new QAction(this); 0078 m_addAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); 0079 m_addAction->setText(i18nc("@item:inmenu", "Bazaar Add...")); 0080 connect(m_addAction, &QAction::triggered, 0081 this, &FileViewBazaarPlugin::addFiles); 0082 0083 m_removeAction = new QAction(this); 0084 m_removeAction->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); 0085 m_removeAction->setText(i18nc("@item:inmenu", "Bazaar Delete")); 0086 connect(m_removeAction, &QAction::triggered, 0087 this, &FileViewBazaarPlugin::removeFiles); 0088 0089 m_logAction = new QAction(this); 0090 m_logAction->setIcon(QIcon::fromTheme(QStringLiteral("format-list-ordered"))); 0091 m_logAction->setText(i18nc("@item:inmenu", "Bazaar Log")); 0092 connect(m_logAction, &QAction::triggered, 0093 this, &FileViewBazaarPlugin::log); 0094 0095 connect(&m_process, &QProcess::finished, 0096 this, &FileViewBazaarPlugin::slotOperationCompleted); 0097 connect(&m_process, &QProcess::errorOccurred, 0098 this, &FileViewBazaarPlugin::slotOperationError); 0099 } 0100 0101 FileViewBazaarPlugin::~FileViewBazaarPlugin() 0102 { 0103 } 0104 0105 QString FileViewBazaarPlugin::fileName() const 0106 { 0107 return QStringLiteral(".bzr"); 0108 } 0109 0110 bool FileViewBazaarPlugin::beginRetrieval(const QString& directory) 0111 { 0112 Q_ASSERT(directory.endsWith(QLatin1Char('/'))); 0113 0114 QString baseDir; 0115 QProcess process1; 0116 process1.setWorkingDirectory(directory); 0117 process1.start(QStringLiteral("bzr"), {QStringLiteral("root")}); 0118 while (process1.waitForReadyRead()) { 0119 char buffer[512]; 0120 while (process1.readLine(buffer, sizeof(buffer)) > 0) { 0121 baseDir = QString::fromLocal8Bit(buffer).trimmed(); 0122 } 0123 } 0124 // if bzr is not installed 0125 if (baseDir.isEmpty()) { 0126 return false; 0127 } 0128 0129 // Clear all entries for this directory including the entries 0130 // for sub directories 0131 QMutableHashIterator<QString, ItemVersion> it(m_versionInfoHash); 0132 while (it.hasNext()) { 0133 it.next(); 0134 if (it.key().startsWith(directory) || !it.key().startsWith(baseDir)) { 0135 it.remove(); 0136 } 0137 } 0138 0139 QProcess process2; 0140 process2.setWorkingDirectory(directory); 0141 process2.start(QStringLiteral("bzr"), {QStringLiteral("ignored")}); 0142 while (process2.waitForReadyRead()) { 0143 char buffer[512]; 0144 while (process2.readLine(buffer, sizeof(buffer)) > 0) { 0145 QString line = QString::fromLocal8Bit(buffer).trimmed(); 0146 QStringList list = line.split(QLatin1Char(' ')); 0147 QString file = baseDir + QLatin1Char('/') + list[0]; 0148 m_versionInfoHash.insert(file, UnversionedVersion); 0149 } 0150 } 0151 0152 const QStringList arguments{ 0153 QStringLiteral("status"), 0154 QStringLiteral("-S"), 0155 baseDir, 0156 }; 0157 0158 QProcess process; 0159 process.start(QStringLiteral("bzr"), arguments); 0160 while (process.waitForReadyRead()) { 0161 char buffer[1024]; 0162 while (process.readLine(buffer, sizeof(buffer)) > 0) { 0163 ItemVersion state = NormalVersion; 0164 QString filePath = QString::fromUtf8(buffer); 0165 0166 // This could probably do with being more consistent 0167 switch (buffer[0]) { 0168 case '?': state = UnversionedVersion; break; 0169 case ' ': if (buffer[1] == 'M') {state = LocallyModifiedVersion;} break; 0170 case '+': state = AddedVersion; break; 0171 case '-': state = RemovedVersion; break; 0172 case 'C': state = ConflictingVersion; break; 0173 default: 0174 if (filePath.contains(QLatin1Char('*'))) { 0175 state = UpdateRequiredVersion; 0176 } 0177 break; 0178 } 0179 0180 // Only values with a different state as 'NormalVersion' 0181 // are added to the hash table. If a value is not in the 0182 // hash table, it is automatically defined as 'NormalVersion' 0183 // (see FileViewBazaarPlugin::versionState()). 0184 if (state != NormalVersion) { 0185 int pos = 4; 0186 const int length = filePath.length() - pos - 1; 0187 //conflicts annoyingly have a human readable text before the filename 0188 //TODO cover other conflict types 0189 if (filePath.startsWith(QLatin1String("C Text conflict"))) { 0190 filePath = filePath.mid(17, length); 0191 } 0192 filePath = baseDir + QLatin1Char('/') + filePath.mid(pos, length); 0193 //remove type symbols from directories, links and executables 0194 if (filePath.endsWith(QLatin1Char('/')) || filePath.endsWith(QLatin1Char('@')) || filePath.endsWith(QLatin1Char('*'))) { 0195 filePath = filePath.left(filePath.length() - 1); 0196 } 0197 if (!filePath.isEmpty()) { 0198 m_versionInfoHash.insert(filePath, state); 0199 } 0200 } 0201 } 0202 } 0203 if ((process.exitCode() != 0 || process.exitStatus() != QProcess::NormalExit)) { 0204 return false; 0205 } 0206 0207 return true; 0208 } 0209 0210 void FileViewBazaarPlugin::endRetrieval() 0211 { 0212 } 0213 0214 KVersionControlPlugin::ItemVersion FileViewBazaarPlugin::itemVersion(const KFileItem& item) const 0215 { 0216 const QString itemUrl = item.localPath(); 0217 if (m_versionInfoHash.contains(itemUrl)) { 0218 return m_versionInfoHash.value(itemUrl); 0219 } 0220 0221 if (!item.isDir()) { 0222 // files that have not been listed by 'bzr status' or 'bzr ignored' (= m_versionInfoHash) 0223 // are under version control per definition 0224 return NormalVersion; 0225 } 0226 0227 // The item is a directory. Check whether an item listed by 'bzr status' (= m_versionInfoHash) 0228 // is part of this directory. In this case a local modification should be indicated in the 0229 // directory already. 0230 const QString itemDir = itemUrl + QDir::separator(); 0231 QHash<QString, ItemVersion>::const_iterator it = m_versionInfoHash.constBegin(); 0232 while (it != m_versionInfoHash.constEnd()) { 0233 if (it.key().startsWith(itemDir)) { 0234 const ItemVersion state = m_versionInfoHash.value(it.key()); 0235 if (state == LocallyModifiedVersion) { 0236 return LocallyModifiedVersion; 0237 } 0238 } 0239 ++it; 0240 } 0241 0242 return NormalVersion; 0243 } 0244 0245 QList<QAction*> FileViewBazaarPlugin::versionControlActions(const KFileItemList &items) const 0246 { 0247 if (items.count() == 1 && items.first().isDir()) { 0248 QString directory = items.first().localPath(); 0249 if (!directory.endsWith(QLatin1Char('/'))) { 0250 directory += QLatin1Char('/'); 0251 } 0252 0253 if (directory == m_contextDir) { 0254 return contextMenuDirectoryActions(directory); 0255 } else { 0256 return contextMenuFilesActions(items); 0257 } 0258 } else { 0259 return contextMenuFilesActions(items); 0260 } 0261 } 0262 0263 QList<QAction*> FileViewBazaarPlugin::outOfVersionControlActions(const KFileItemList& items) const 0264 { 0265 Q_UNUSED(items) 0266 0267 return {}; 0268 } 0269 0270 QList<QAction*> FileViewBazaarPlugin::contextMenuFilesActions(const KFileItemList& items) const 0271 { 0272 Q_ASSERT(!items.isEmpty()); 0273 for (const KFileItem& item : items) { 0274 m_contextItems.append(item); 0275 } 0276 m_contextDir.clear(); 0277 0278 const bool noPendingOperation = !m_pendingOperation; 0279 if (noPendingOperation) { 0280 // iterate all items and check the version state to know which 0281 // actions can be enabled 0282 const int itemsCount = items.count(); 0283 int versionedCount = 0; 0284 int editingCount = 0; 0285 for (const KFileItem& item : items) { 0286 const ItemVersion state = itemVersion(item); 0287 if (state != UnversionedVersion) { 0288 ++versionedCount; 0289 } 0290 0291 switch (state) { 0292 case LocallyModifiedVersion: 0293 case ConflictingVersion: 0294 ++editingCount; 0295 break; 0296 default: 0297 break; 0298 } 0299 } 0300 m_commitAction->setEnabled(editingCount > 0); 0301 m_addAction->setEnabled(versionedCount == 0); 0302 m_removeAction->setEnabled(versionedCount == itemsCount); 0303 } else { 0304 m_commitAction->setEnabled(false); 0305 m_addAction->setEnabled(false); 0306 m_removeAction->setEnabled(false); 0307 } 0308 m_updateAction->setEnabled(noPendingOperation); 0309 m_pullAction->setEnabled(noPendingOperation); 0310 m_pushAction->setEnabled(noPendingOperation); 0311 m_showLocalChangesAction->setEnabled(noPendingOperation); 0312 m_logAction->setEnabled(noPendingOperation); 0313 0314 QList<QAction*> actions; 0315 actions.append(m_updateAction); 0316 actions.append(m_pullAction); 0317 actions.append(m_pushAction); 0318 actions.append(m_commitAction); 0319 actions.append(m_addAction); 0320 actions.append(m_removeAction); 0321 actions.append(m_showLocalChangesAction); 0322 actions.append(m_logAction); 0323 return actions; 0324 } 0325 0326 QList<QAction*> FileViewBazaarPlugin::contextMenuDirectoryActions(const QString& directory) const 0327 { 0328 m_contextDir = directory; 0329 m_contextItems.clear(); 0330 0331 // Only enable the actions if no commands are 0332 // executed currently (see slotOperationCompleted() and 0333 // startBazaarCommandProcess()). 0334 const bool enabled = !m_pendingOperation; 0335 m_updateAction->setEnabled(enabled); 0336 m_pullAction->setEnabled(enabled); 0337 m_pushAction->setEnabled(enabled); 0338 m_commitAction->setEnabled(enabled); 0339 m_addAction->setEnabled(enabled); 0340 m_showLocalChangesAction->setEnabled(enabled); 0341 m_logAction->setEnabled(enabled); 0342 0343 QList<QAction*> actions; 0344 actions.append(m_updateAction); 0345 actions.append(m_pullAction); 0346 actions.append(m_pushAction); 0347 actions.append(m_commitAction); 0348 actions.append(m_addAction); 0349 actions.append(m_showLocalChangesAction); 0350 actions.append(m_logAction); 0351 return actions; 0352 } 0353 0354 void FileViewBazaarPlugin::updateFiles() 0355 { 0356 execBazaarCommand(QStringLiteral("qupdate"), QStringList(), 0357 i18nc("@info:status", "Updating Bazaar repository..."), 0358 i18nc("@info:status", "Update of Bazaar repository failed."), 0359 i18nc("@info:status", "Updated Bazaar repository.")); 0360 } 0361 0362 void FileViewBazaarPlugin::pullFiles() 0363 { 0364 QStringList arguments = QStringList(); 0365 arguments << QStringLiteral("-d"); 0366 execBazaarCommand(QStringLiteral("qpull"), arguments, 0367 i18nc("@info:status", "Pulling Bazaar repository..."), 0368 i18nc("@info:status", "Pull of Bazaar repository failed."), 0369 i18nc("@info:status", "Pulled Bazaar repository.")); 0370 } 0371 0372 void FileViewBazaarPlugin::pushFiles() 0373 { 0374 QStringList arguments = QStringList(); 0375 arguments << QStringLiteral("-d"); 0376 execBazaarCommand(QStringLiteral("qpush"), arguments, 0377 i18nc("@info:status", "Pushing Bazaar repository..."), 0378 i18nc("@info:status", "Push of Bazaar repository failed."), 0379 i18nc("@info:status", "Pushed Bazaar repository.")); 0380 } 0381 0382 void FileViewBazaarPlugin::showLocalChanges() 0383 { 0384 execBazaarCommand(QStringLiteral("qdiff"), QStringList(), 0385 i18nc("@info:status", "Reviewing Changes..."), 0386 i18nc("@info:status", "Review Changes failed."), 0387 i18nc("@info:status", "Reviewed Changes.")); 0388 } 0389 0390 void FileViewBazaarPlugin::commitFiles() 0391 { 0392 execBazaarCommand(QStringLiteral("qcommit"), QStringList(), 0393 i18nc("@info:status", "Committing Bazaar changes..."), 0394 i18nc("@info:status", "Commit of Bazaar changes failed."), 0395 i18nc("@info:status", "Committed Bazaar changes.")); 0396 } 0397 0398 void FileViewBazaarPlugin::addFiles() 0399 { 0400 execBazaarCommand(QStringLiteral("qadd"), QStringList(), 0401 i18nc("@info:status", "Adding files to Bazaar repository..."), 0402 i18nc("@info:status", "Adding of files to Bazaar repository failed."), 0403 i18nc("@info:status", "Added files to Bazaar repository.")); 0404 } 0405 0406 void FileViewBazaarPlugin::removeFiles() 0407 { 0408 execBazaarCommand(QStringLiteral("remove"), QStringList(), 0409 i18nc("@info:status", "Removing files from Bazaar repository..."), 0410 i18nc("@info:status", "Removing of files from Bazaar repository failed."), 0411 i18nc("@info:status", "Removed files from Bazaar repository.")); 0412 } 0413 0414 void FileViewBazaarPlugin::log() 0415 { 0416 execBazaarCommand(QStringLiteral("qlog"), QStringList(), 0417 i18nc("@info:status", "Running Bazaar Log..."), 0418 i18nc("@info:status", "Running Bazaar Log failed."), 0419 i18nc("@info:status", "Bazaar Log closed.")); 0420 } 0421 0422 void FileViewBazaarPlugin::slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus) 0423 { 0424 m_pendingOperation = false; 0425 0426 if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { 0427 Q_EMIT errorMessage(m_errorMsg); 0428 } else if (m_contextItems.isEmpty()) { 0429 Q_EMIT operationCompletedMessage(m_operationCompletedMsg); 0430 Q_EMIT itemVersionsChanged(); 0431 } else { 0432 startBazaarCommandProcess(); 0433 } 0434 } 0435 0436 void FileViewBazaarPlugin::slotOperationError() 0437 { 0438 // don't do any operation on other items anymore 0439 m_contextItems.clear(); 0440 m_pendingOperation = false; 0441 0442 Q_EMIT errorMessage(m_errorMsg); 0443 } 0444 0445 void FileViewBazaarPlugin::execBazaarCommand(const QString& command, 0446 const QStringList& arguments, 0447 const QString& infoMsg, 0448 const QString& errorMsg, 0449 const QString& operationCompletedMsg) 0450 { 0451 Q_EMIT infoMessage(infoMsg); 0452 0453 QProcess process; 0454 process.start(QStringLiteral("bzr"), {QStringLiteral("plugins")}); 0455 bool foundQbzr = false; 0456 while (process.waitForReadyRead()) { 0457 char buffer[512]; 0458 while (process.readLine(buffer, sizeof(buffer)) > 0) { 0459 QString output = QString::fromLocal8Bit(buffer).trimmed(); 0460 if (output.startsWith(QLatin1String("qbzr"))) { 0461 foundQbzr = true; 0462 break; 0463 } 0464 } 0465 } 0466 0467 if (!foundQbzr) { 0468 Q_EMIT infoMessage(QStringLiteral("Please Install QBzr")); 0469 return; 0470 } 0471 0472 m_command = command; 0473 m_arguments = arguments; 0474 m_errorMsg = errorMsg; 0475 m_operationCompletedMsg = operationCompletedMsg; 0476 0477 startBazaarCommandProcess(); 0478 } 0479 0480 void FileViewBazaarPlugin::startBazaarCommandProcess() 0481 { 0482 Q_ASSERT(m_process.state() == QProcess::NotRunning); 0483 m_pendingOperation = true; 0484 0485 const QString program(QStringLiteral("bzr")); 0486 QStringList arguments; 0487 arguments << m_command << m_arguments; 0488 if (!m_contextDir.isEmpty()) { 0489 arguments << m_contextDir; 0490 m_contextDir.clear(); 0491 } else { 0492 const KFileItem item = m_contextItems.takeLast(); 0493 arguments << item.localPath(); 0494 // the remaining items of m_contextItems will be executed 0495 // after the process has finished (see slotOperationFinished()) 0496 } 0497 m_process.start(program, arguments); 0498 } 0499 0500 #include "fileviewbazaarplugin.moc" 0501 0502 #include "moc_fileviewbazaarplugin.cpp"