File indexing completed on 2024-04-14 05:34:12
0001 /* 0002 SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz@gmx.at> 0003 SPDX-FileCopyrightText: 2010 Sebastian Doerner <sebastian@sebastian-doerner.de> 0004 SPDX-FileCopyrightText: 2010 Johannes Steffen <jsteffen@st.ovgu.de> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "fileviewgitplugin.h" 0010 #include "checkoutdialog.h" 0011 #include "commitdialog.h" 0012 #include "tagdialog.h" 0013 #include "pushdialog.h" 0014 #include "gitwrapper.h" 0015 #include "pulldialog.h" 0016 0017 #include <KLocalizedString> 0018 #include <KShell> 0019 #include <KPluginFactory> 0020 0021 #include <QTemporaryFile> 0022 #include <QProcess> 0023 #include <QString> 0024 #include <QStringList> 0025 #include <QTextCodec> 0026 #include <QDir> 0027 #include <QTextBrowser> 0028 #include <QVBoxLayout> 0029 0030 K_PLUGIN_CLASS_WITH_JSON(FileViewGitPlugin, "fileviewgitplugin.json") 0031 0032 FileViewGitPlugin::FileViewGitPlugin(QObject* parent, const QList<QVariant>& args) : 0033 KVersionControlPlugin(parent), 0034 m_pendingOperation(false), 0035 m_addAction(nullptr), 0036 m_removeAction(nullptr), 0037 m_checkoutAction(nullptr), 0038 m_commitAction(nullptr), 0039 m_tagAction(nullptr), 0040 m_pushAction(nullptr), 0041 m_pullAction(nullptr) 0042 { 0043 Q_UNUSED(args); 0044 0045 m_parentWidget = qobject_cast<QWidget*>(parent); 0046 0047 m_revertAction = new QAction(this); 0048 m_revertAction->setIcon(QIcon::fromTheme(QStringLiteral("document-revert"))); 0049 m_revertAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Revert")); 0050 connect(m_revertAction, &QAction::triggered, 0051 this, &FileViewGitPlugin::revertFiles); 0052 0053 m_addAction = new QAction(this); 0054 m_addAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); 0055 m_addAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Add")); 0056 connect(m_addAction, &QAction::triggered, 0057 this, &FileViewGitPlugin::addFiles); 0058 0059 m_showLocalChangesAction = new QAction(this); 0060 m_showLocalChangesAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-diff"))); 0061 m_showLocalChangesAction->setText(xi18nd("@item:inmenu", "Show Local <application>Git</application> Changes")); 0062 connect(m_showLocalChangesAction, &QAction::triggered, 0063 this, &FileViewGitPlugin::showLocalChanges); 0064 0065 m_removeAction = new QAction(this); 0066 m_removeAction->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); 0067 m_removeAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Remove")); 0068 connect(m_removeAction, &QAction::triggered, 0069 this, &FileViewGitPlugin::removeFiles); 0070 0071 m_checkoutAction = new QAction(this); 0072 m_checkoutAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-branch"))); 0073 m_checkoutAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Checkout...")); 0074 connect(m_checkoutAction, &QAction::triggered, 0075 this, &FileViewGitPlugin::checkout); 0076 0077 m_commitAction = new QAction(this); 0078 m_commitAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-commit"))); 0079 m_commitAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Commit...")); 0080 connect(m_commitAction, &QAction::triggered, 0081 this, &FileViewGitPlugin::commit); 0082 0083 m_tagAction = new QAction(this); 0084 // m_tagAction->setIcon(QIcon::fromTheme(QStringLiteral("svn-commit"))); 0085 m_tagAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Create Tag...")); 0086 connect(m_tagAction, &QAction::triggered, 0087 this, &FileViewGitPlugin::createTag); 0088 m_pushAction = new QAction(this); 0089 m_pushAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-push"))); 0090 m_pushAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Push...")); 0091 connect(m_pushAction, &QAction::triggered, 0092 this, &FileViewGitPlugin::push); 0093 m_pullAction = new QAction(this); 0094 m_pullAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-pull"))); 0095 m_pullAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Pull...")); 0096 connect(m_pullAction, &QAction::triggered, 0097 this, &FileViewGitPlugin::pull); 0098 m_mergeAction = new QAction(this); 0099 m_mergeAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-merge"))); 0100 m_mergeAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Merge...")); 0101 connect(m_mergeAction, &QAction::triggered, this, &FileViewGitPlugin::merge); 0102 0103 m_logAction = new QAction(this); 0104 m_logAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Log...")); 0105 connect(m_logAction, &QAction::triggered, this, &FileViewGitPlugin::log); 0106 0107 connect(&m_process, &QProcess::finished, 0108 this, &FileViewGitPlugin::slotOperationCompleted); 0109 connect(&m_process, &QProcess::errorOccurred, 0110 this, &FileViewGitPlugin::slotOperationError); 0111 } 0112 0113 FileViewGitPlugin::~FileViewGitPlugin() 0114 { 0115 GitWrapper::freeInstance(); 0116 } 0117 0118 QString FileViewGitPlugin::fileName() const 0119 { 0120 return QLatin1String(".git"); 0121 } 0122 0123 QString FileViewGitPlugin::localRepositoryRoot(const QString& directory) const 0124 { 0125 QProcess process; 0126 process.setWorkingDirectory(directory); 0127 process.start(QStringLiteral("git"), {QStringLiteral("rev-parse"), QStringLiteral("--show-toplevel")}); 0128 if (process.waitForReadyRead(100) && process.exitCode() == 0) { 0129 return QString::fromUtf8(process.readAll().chopped(1)); 0130 } 0131 return QString(); 0132 } 0133 0134 int FileViewGitPlugin::readUntilZeroChar(QIODevice* device, char* buffer, const int maxChars) { 0135 if (buffer == nullptr) { // discard until next \0 0136 char c; 0137 while (device->getChar(&c) && c != '\0') 0138 ; 0139 return 0; 0140 } 0141 int index = -1; 0142 while (++index < maxChars) { 0143 if (!device->getChar(&buffer[index])) { 0144 if (device->waitForReadyRead(30000)) { // 30 seconds to be consistent with QProcess::waitForReadyRead default 0145 --index; 0146 continue; 0147 } else { 0148 buffer[index] = '\0'; 0149 return index <= 0 ? 0 : index + 1; 0150 } 0151 } 0152 if (buffer[index] == '\0') { // line end or we put it there (see above) 0153 return index + 1; 0154 } 0155 } 0156 return maxChars; 0157 } 0158 0159 bool FileViewGitPlugin::beginRetrieval(const QString& directory) 0160 { 0161 Q_ASSERT(directory.endsWith(QLatin1Char('/'))); 0162 0163 GitWrapper::instance()->setWorkingDirectory(directory); 0164 m_currentDir = directory; 0165 0166 // ----- find path below git base dir ----- 0167 QProcess process; 0168 process.setWorkingDirectory(directory); 0169 process.start(QStringLiteral("git"), {QStringLiteral("rev-parse"), QStringLiteral("--show-prefix")}); 0170 QString dirBelowBaseDir; 0171 while (process.waitForReadyRead()) { 0172 char buffer[512]; 0173 while (process.readLine(buffer, sizeof(buffer)) > 0) { 0174 dirBelowBaseDir = QString::fromLocal8Bit(buffer).trimmed(); // ends in "/" or is empty 0175 } 0176 } 0177 0178 m_versionInfoHash.clear(); 0179 0180 // ----- find files with special status ----- 0181 process.start(QStringLiteral("git"), {QStringLiteral("--no-optional-locks"), QStringLiteral("status"), QStringLiteral("--porcelain"), QStringLiteral("-z"), QStringLiteral("-u"), QStringLiteral("--ignored")}); 0182 while (process.waitForReadyRead()) { 0183 char buffer[1024]; 0184 while (readUntilZeroChar(&process, buffer, sizeof(buffer)) > 0 ) { 0185 QString line = QTextCodec::codecForLocale()->toUnicode(buffer); 0186 // ----- recognize file status ----- 0187 char X = line[0].toLatin1(); // X and Y from the table in `man git-status` 0188 char Y = line[1].toLatin1(); 0189 const QString fileName= line.mid(3); 0190 ItemVersion state = NormalVersion; 0191 switch (X) { 0192 case '!': 0193 state = IgnoredVersion; 0194 break; 0195 case '?': 0196 state = UnversionedVersion; 0197 break; 0198 case 'C': // handle copied as added version 0199 case 'A': 0200 state = AddedVersion; 0201 break; 0202 case 'D': 0203 state = RemovedVersion; 0204 break; 0205 case 'M': 0206 state = LocallyModifiedVersion; 0207 break; 0208 case 'R': 0209 state = LocallyModifiedVersion; 0210 // Renames list the old file name directly afterwards, separated by \0. 0211 readUntilZeroChar(&process, nullptr, 0); // discard old file name 0212 break; 0213 } 0214 // overwrite status depending on the working tree 0215 switch (Y) { 0216 case 'D': // handle "deleted in working tree" as "modified in working tree" 0217 case 'M': 0218 state = LocallyModifiedUnstagedVersion; 0219 break; 0220 } 0221 // overwrite status in case of conflicts (lower part of the table in `man git-status`) 0222 if (X == 'U' || 0223 Y == 'U' || 0224 (X == 'A' && Y == 'A') || 0225 (X == 'D' && Y == 'D')) { 0226 state = ConflictingVersion; 0227 } 0228 0229 // ----- decide what to record about that file ----- 0230 if (state == NormalVersion || !fileName.startsWith(dirBelowBaseDir)) { 0231 continue; 0232 } 0233 /// File name relative to the current working directory. 0234 const QString relativeFileName = fileName.mid(dirBelowBaseDir.length()); 0235 //if file is part of a sub-directory, record the directory 0236 if (relativeFileName.contains(QLatin1Char('/'))) { 0237 if (state == IgnoredVersion) 0238 continue; 0239 if (state == AddedVersion || state == RemovedVersion) { 0240 state = LocallyModifiedVersion; 0241 } 0242 const QString absoluteDirName = directory + relativeFileName.left(relativeFileName.indexOf(QLatin1Char('/'))); 0243 if (m_versionInfoHash.contains(absoluteDirName)) { 0244 ItemVersion oldState = m_versionInfoHash.value(absoluteDirName); 0245 //only keep the most important state for a directory 0246 if (oldState == ConflictingVersion) 0247 continue; 0248 if (oldState == LocallyModifiedUnstagedVersion && state != ConflictingVersion) 0249 continue; 0250 if (oldState == LocallyModifiedVersion && 0251 state != LocallyModifiedUnstagedVersion && state != ConflictingVersion) 0252 continue; 0253 m_versionInfoHash.insert(absoluteDirName, state); 0254 } else { 0255 m_versionInfoHash.insert(absoluteDirName, state); 0256 } 0257 } else { //normal file, no directory 0258 m_versionInfoHash.insert(directory + relativeFileName, state); 0259 } 0260 } 0261 } 0262 return true; 0263 } 0264 0265 void FileViewGitPlugin::endRetrieval() 0266 { 0267 } 0268 0269 KVersionControlPlugin::ItemVersion FileViewGitPlugin::itemVersion(const KFileItem& item) const 0270 { 0271 const QString itemUrl = item.localPath(); 0272 if (m_versionInfoHash.contains(itemUrl)) { 0273 return m_versionInfoHash.value(itemUrl); 0274 } else { 0275 // files that are not in our map are normal, tracked files by definition 0276 return NormalVersion; 0277 } 0278 } 0279 0280 QList<QAction*> FileViewGitPlugin::versionControlActions(const KFileItemList& items) const 0281 { 0282 if (items.count() == 1 && items.first().isDir()) { 0283 QString directory = items.first().localPath(); 0284 if (!directory.endsWith(QLatin1Char('/'))) { 0285 directory += QLatin1Char('/'); 0286 } 0287 0288 if (directory == m_currentDir) { 0289 return contextMenuDirectoryActions(directory); 0290 } else { 0291 return contextMenuFilesActions(items); 0292 } 0293 } else { 0294 return contextMenuFilesActions(items); 0295 } 0296 } 0297 0298 QList<QAction*> FileViewGitPlugin::outOfVersionControlActions(const KFileItemList& items) const 0299 { 0300 Q_UNUSED(items) 0301 0302 return {}; 0303 } 0304 0305 QList<QAction*> FileViewGitPlugin::contextMenuFilesActions(const KFileItemList& items) const 0306 { 0307 Q_ASSERT(!items.isEmpty()); 0308 0309 if (!m_pendingOperation){ 0310 m_contextDir = QFileInfo(items.first().localPath()).canonicalPath(); 0311 m_contextItems.clear(); 0312 for (const KFileItem& item : items){ 0313 m_contextItems.append(item); 0314 } 0315 0316 //see which actions should be enabled 0317 int versionedCount = 0; 0318 int addableCount = 0; 0319 int revertCount = 0; 0320 for (const KFileItem& item : items){ 0321 const ItemVersion state = itemVersion(item); 0322 if (state != UnversionedVersion && state != RemovedVersion && 0323 state != IgnoredVersion) { 0324 ++versionedCount; 0325 } 0326 if (state == UnversionedVersion || state == LocallyModifiedUnstagedVersion || 0327 state == IgnoredVersion || state == ConflictingVersion) { 0328 ++addableCount; 0329 } 0330 if (state == LocallyModifiedVersion || state == LocallyModifiedUnstagedVersion || 0331 state == ConflictingVersion) { 0332 ++revertCount; 0333 } 0334 } 0335 0336 m_logAction->setEnabled(versionedCount == items.count()); 0337 m_addAction->setEnabled(addableCount == items.count()); 0338 m_revertAction->setEnabled(revertCount == items.count()); 0339 m_removeAction->setEnabled(versionedCount == items.count()); 0340 } 0341 else{ 0342 m_logAction->setEnabled(false); 0343 m_addAction->setEnabled(false); 0344 m_revertAction->setEnabled(false); 0345 m_removeAction->setEnabled(false); 0346 } 0347 0348 QList<QAction*> actions; 0349 actions.append(m_logAction); 0350 actions.append(m_addAction); 0351 actions.append(m_removeAction); 0352 actions.append(m_revertAction); 0353 return actions; 0354 } 0355 0356 QList<QAction*> FileViewGitPlugin::contextMenuDirectoryActions(const QString& directory) const 0357 { 0358 QList<QAction*> actions; 0359 if (!m_pendingOperation) { 0360 m_contextItems.clear(); 0361 m_contextDir = directory; 0362 } 0363 m_checkoutAction->setEnabled(!m_pendingOperation); 0364 actions.append(m_checkoutAction); 0365 0366 bool canCommit = false; 0367 bool showChanges = false; 0368 bool shouldMerge = false; 0369 QHash<QString, ItemVersion>::const_iterator it = m_versionInfoHash.constBegin(); 0370 while (it != m_versionInfoHash.constEnd()) { 0371 const ItemVersion state = it.value(); 0372 if (state == LocallyModifiedVersion || state == AddedVersion || state == RemovedVersion) { 0373 canCommit = true; 0374 } 0375 if (state == LocallyModifiedUnstagedVersion || state == LocallyModifiedVersion) { 0376 showChanges = true; 0377 } 0378 if (state == ConflictingVersion) { 0379 canCommit = false; 0380 showChanges = true; 0381 shouldMerge = true; 0382 break; 0383 } 0384 ++it; 0385 } 0386 0387 m_logAction->setEnabled(!m_pendingOperation); 0388 actions.append(m_logAction); 0389 0390 m_showLocalChangesAction->setEnabled(!m_pendingOperation && showChanges); 0391 actions.append(m_showLocalChangesAction); 0392 0393 if (!shouldMerge) { 0394 m_commitAction->setEnabled(!m_pendingOperation && canCommit); 0395 actions.append(m_commitAction); 0396 } else { 0397 m_mergeAction->setEnabled(!m_pendingOperation); 0398 actions.append(m_mergeAction); 0399 } 0400 0401 m_tagAction->setEnabled(!m_pendingOperation); 0402 actions.append(m_tagAction); 0403 m_pushAction->setEnabled(!m_pendingOperation); 0404 actions.append(m_pushAction); 0405 m_pullAction->setEnabled(!m_pendingOperation); 0406 actions.append(m_pullAction); 0407 0408 return actions; 0409 } 0410 0411 void FileViewGitPlugin::addFiles() 0412 { 0413 execGitCommand(QStringLiteral("add"), QStringList(), 0414 xi18nd("@info:status", "Adding files to <application>Git</application> repository..."), 0415 xi18nd("@info:status", "Adding files to <application>Git</application> repository failed."), 0416 xi18nd("@info:status", "Added files to <application>Git</application> repository.")); 0417 } 0418 0419 void FileViewGitPlugin::removeFiles() 0420 { 0421 const QStringList arguments{ 0422 QStringLiteral("-r"), //recurse through directories 0423 QStringLiteral("--force"), //also remove files that have not been committed yet 0424 }; 0425 execGitCommand(QStringLiteral("rm"), arguments, 0426 xi18nd("@info:status", "Removing files from <application>Git</application> repository..."), 0427 xi18nd("@info:status", "Removing files from <application>Git</application> repository failed."), 0428 xi18nd("@info:status", "Removed files from <application>Git</application> repository.")); 0429 } 0430 0431 void FileViewGitPlugin::revertFiles() 0432 { 0433 execGitCommand(QStringLiteral("checkout"), { QStringLiteral("--") }, 0434 xi18nd("@info:status", "Reverting files from <application>Git</application> repository..."), 0435 xi18nd("@info:status", "Reverting files from <application>Git</application> repository failed."), 0436 xi18nd("@info:status", "Reverted files from <application>Git</application> repository.")); 0437 } 0438 0439 void FileViewGitPlugin::showLocalChanges() 0440 { 0441 Q_ASSERT(!m_contextDir.isEmpty()); 0442 0443 runCommand(QStringLiteral("git difftool --dir-diff .")); 0444 } 0445 0446 void FileViewGitPlugin::showDiff(const QUrl &link) 0447 { 0448 if (link.scheme() != QLatin1String("rev")) { 0449 return; 0450 } 0451 runCommand(QStringLiteral("git difftool --dir-diff %1^ %1").arg(link.path())); 0452 } 0453 0454 void FileViewGitPlugin::log() 0455 { 0456 QStringList items; 0457 if (m_contextItems.isEmpty()) { 0458 items << QStringLiteral("."); 0459 } else { 0460 for (auto &item : std::as_const(m_contextItems)) { 0461 items << item.url().fileName(); 0462 } 0463 } 0464 0465 QProcess process; 0466 process.setWorkingDirectory(m_contextDir); 0467 process.start( 0468 QStringLiteral("git"), 0469 QStringList { 0470 QStringLiteral("log"), 0471 QStringLiteral("--date=format:%d-%m-%Y"), 0472 QStringLiteral("-n 100"), 0473 QStringLiteral("--pretty=format:<tr> <td><a href=\"rev:%h\">%h</a></td> <td>%ad</td> <td>%s</td> <td>%an</td> </tr>") 0474 } + items 0475 ); 0476 0477 if (!process.waitForFinished() || process.exitCode() != 0) { 0478 Q_EMIT errorMessage(xi18nd("@info:status", "<application>Git</application> Log failed.")); 0479 return; 0480 } 0481 0482 const QString gitOutput = QString::fromLocal8Bit(process.readAllStandardOutput()); 0483 0484 QPalette palette; 0485 const QString styleSheet = QStringLiteral( 0486 "body { background: %1; color: %2; }" \ 0487 "table.logtable td { padding: 9px 8px 9px; }" \ 0488 "a { color: %3; }" \ 0489 "a:visited { color: %4; } " 0490 ).arg(palette.window().color().name(), 0491 palette.text().color().name(), 0492 palette.link().color().name(), 0493 palette.linkVisited().color().name()); 0494 0495 QDialog* dlg = new QDialog(m_parentWidget); 0496 QVBoxLayout* layout = new QVBoxLayout; 0497 auto view = new QTextBrowser(dlg); 0498 layout->addWidget(view); 0499 dlg->setLayout(layout); 0500 dlg->setAttribute(Qt::WA_DeleteOnClose); 0501 dlg->setWindowTitle(xi18nd("@title:window", "<application>Git</application> Log")); 0502 view->setOpenLinks(false); 0503 view->setOpenExternalLinks(false); 0504 connect(view, &QTextBrowser::anchorClicked, this, &FileViewGitPlugin::showDiff); 0505 view->setHtml(QStringLiteral( 0506 "<html>" \ 0507 "<style> %1 </style>" \ 0508 "<table class=\"logtable\">" \ 0509 "<tr bgcolor=\"%2\">" \ 0510 "<td> %3 </td> <td> %4 </td> <td> %5 </p> </td> <td> %6 </td>" \ 0511 "</tr>" \ 0512 "%7" \ 0513 "</table>" \ 0514 "</html>" 0515 ).arg(styleSheet, 0516 palette.highlight().color().name(), 0517 i18nc("Git commit hash", "Commit"), 0518 i18nc("Git commit date", "Date"), 0519 i18nc("Git commit message", "Message"), 0520 i18nc("Git commit author", "Author"), 0521 gitOutput)); 0522 0523 dlg->resize(QSize(720, 560)); 0524 dlg->show(); 0525 } 0526 0527 void FileViewGitPlugin::merge() 0528 { 0529 Q_ASSERT(!m_contextDir.isEmpty()); 0530 0531 runCommand(QStringLiteral("git mergetool")); 0532 } 0533 0534 void FileViewGitPlugin::checkout() 0535 { 0536 CheckoutDialog dialog(m_parentWidget); 0537 if (dialog.exec() == QDialog::Accepted){ 0538 QProcess process; 0539 process.setWorkingDirectory(m_contextDir); 0540 QStringList arguments; 0541 arguments << QStringLiteral("checkout"); 0542 if (dialog.force()) { 0543 arguments << QStringLiteral("-f"); 0544 } 0545 const QString newBranchName = dialog.newBranchName(); 0546 if (!newBranchName.isEmpty()) { 0547 arguments << QStringLiteral("-b"); 0548 arguments << newBranchName; 0549 } 0550 const QString checkoutIdentifier = dialog.checkoutIdentifier(); 0551 if (!checkoutIdentifier.isEmpty()) { 0552 arguments << checkoutIdentifier; 0553 } 0554 //to appear in messages 0555 const QString currentBranchName = newBranchName.isEmpty() ? checkoutIdentifier : newBranchName; 0556 process.start(QStringLiteral("git"), arguments); 0557 process.setReadChannel(QProcess::StandardError); //git writes info messages to stderr as well 0558 QString completedMessage; 0559 while (process.waitForReadyRead()) { 0560 char buffer[512]; 0561 while (process.readLine(buffer, sizeof(buffer)) > 0){ 0562 const QString currentLine = QString::fromLocal8Bit(buffer); 0563 if (currentLine.startsWith(QLatin1String("Switched to branch"))) { 0564 completedMessage = xi18nd("@info:status", "Switched to branch '%1'", currentBranchName); 0565 } 0566 if (currentLine.startsWith(QLatin1String("HEAD is now at"))) { 0567 const QString headIdentifier = currentLine. 0568 mid(QLatin1String("HEAD is now at ").size()).trimmed(); 0569 completedMessage = xi18nd("@info:status Git HEAD pointer, parameter includes " 0570 "short SHA-1 & commit message ", "HEAD is now at %1", headIdentifier); 0571 } 0572 //special output for checkout -b 0573 if (currentLine.startsWith(QLatin1String("Switched to a new branch"))) { 0574 completedMessage = xi18nd("@info:status", "Switched to a new branch '%1'", currentBranchName); 0575 } 0576 } 0577 } 0578 if (process.exitCode() == 0 && process.exitStatus() == QProcess::NormalExit) { 0579 if (!completedMessage.isEmpty()) { 0580 Q_EMIT operationCompletedMessage(completedMessage); 0581 Q_EMIT itemVersionsChanged(); 0582 } 0583 } 0584 else { 0585 Q_EMIT errorMessage(xi18nd("@info:status", "<application>Git</application> Checkout failed." 0586 " Maybe your working directory is dirty.")); 0587 } 0588 } 0589 } 0590 0591 void FileViewGitPlugin::commit() 0592 { 0593 CommitDialog dialog(m_parentWidget); 0594 if (dialog.exec() == QDialog::Accepted) { 0595 QTemporaryFile tmpCommitMessageFile; 0596 tmpCommitMessageFile.open(); 0597 tmpCommitMessageFile.write(dialog.commitMessage()); 0598 tmpCommitMessageFile.close(); 0599 QProcess process; 0600 process.setWorkingDirectory(m_contextDir); 0601 QStringList args = {QStringLiteral("commit")}; 0602 if (dialog.amend()) { 0603 args << QStringLiteral("--amend"); 0604 } 0605 args << QStringLiteral("-F"); 0606 args << tmpCommitMessageFile.fileName(); 0607 process.start(QStringLiteral("git"), args); 0608 QString completedMessage; 0609 while (process.waitForReadyRead()){ 0610 char buffer[512]; 0611 while (process.readLine(buffer, sizeof(buffer)) > 0) { 0612 if (strlen(buffer) > 0 && buffer[0] == '[') { 0613 completedMessage = QTextCodec::codecForLocale()->toUnicode(buffer).trimmed(); 0614 break; 0615 } 0616 } 0617 } 0618 if (!completedMessage.isEmpty()) { 0619 Q_EMIT operationCompletedMessage(completedMessage); 0620 Q_EMIT itemVersionsChanged(); 0621 } 0622 } 0623 } 0624 0625 void FileViewGitPlugin::createTag() 0626 { 0627 TagDialog dialog(m_parentWidget); 0628 if (dialog.exec() == QDialog::Accepted) { 0629 QTemporaryFile tempTagMessageFile; 0630 tempTagMessageFile.open(); 0631 tempTagMessageFile.write(dialog.tagMessage()); 0632 tempTagMessageFile.close(); 0633 QProcess process; 0634 process.setWorkingDirectory(m_contextDir); 0635 process.setReadChannel(QProcess::StandardError); 0636 process.start(QStringLiteral("git"), {QStringLiteral("tag"), QStringLiteral("-a"), QStringLiteral("-F"), tempTagMessageFile.fileName(), dialog.tagName(), dialog.baseBranch()}); 0637 QString completedMessage; 0638 bool gotTagAlreadyExistsMessage = false; 0639 while (process.waitForReadyRead()) { 0640 char buffer[512]; 0641 while (process.readLine(buffer, sizeof(buffer)) > 0) { 0642 const QString line = QString::fromLocal8Bit(buffer); 0643 if (line.contains(QLatin1String("already exists"))) { 0644 gotTagAlreadyExistsMessage = true; 0645 } 0646 } 0647 } 0648 if (process.exitCode() == 0 && process.exitStatus() == QProcess::NormalExit) { 0649 completedMessage = xi18nd("@info:status","Successfully created tag '%1'", dialog.tagName()); 0650 Q_EMIT operationCompletedMessage(completedMessage); 0651 } else { 0652 //I don't know any other error, but in case one occurs, the user doesn't get FALSE error messages 0653 Q_EMIT errorMessage(gotTagAlreadyExistsMessage ? 0654 xi18nd("@info:status", "<application>Git</application> tag creation failed." 0655 " A tag with the name '%1' already exists.", dialog.tagName()) : 0656 xi18nd("@info:status", "<application>Git</application> tag creation failed.") 0657 ); 0658 } 0659 } 0660 } 0661 0662 void FileViewGitPlugin::push() 0663 { 0664 PushDialog dialog(m_parentWidget); 0665 if (dialog.exec() == QDialog::Accepted) { 0666 m_process.setWorkingDirectory(m_contextDir); 0667 0668 m_errorMsg = xi18nd("@info:status", "Pushing branch %1 to %2:%3 failed.", 0669 dialog.localBranch(), dialog.destination(), dialog.remoteBranch()); 0670 m_operationCompletedMsg = xi18nd("@info:status", "Pushed branch %1 to %2:%3.", 0671 dialog.localBranch(), dialog.destination(), dialog.remoteBranch()); 0672 Q_EMIT infoMessage(xi18nd("@info:status", "Pushing branch %1 to %2:%3...", 0673 dialog.localBranch(), dialog.destination(), dialog.remoteBranch())); 0674 0675 m_command = QStringLiteral("push"); 0676 m_pendingOperation = true; 0677 QStringList args; 0678 args << QStringLiteral("push"); 0679 if (dialog.force()) { 0680 args << QStringLiteral("--force"); 0681 } 0682 args << dialog.destination(); 0683 args << QStringLiteral("%1:%2").arg(dialog.localBranch(), dialog.remoteBranch()); 0684 m_process.start(QStringLiteral("git"), args); 0685 } 0686 } 0687 0688 void FileViewGitPlugin::pull() 0689 { 0690 PullDialog dialog(m_parentWidget); 0691 if (dialog.exec() == QDialog::Accepted) { 0692 m_process.setWorkingDirectory(m_contextDir); 0693 0694 m_errorMsg = xi18nd("@info:status", "Pulling branch %1 from %2 failed.", 0695 dialog.remoteBranch(), dialog.source()); 0696 m_operationCompletedMsg = xi18nd("@info:status", "Pulled branch %1 from %2 successfully.", 0697 dialog.remoteBranch(), dialog.source()); 0698 Q_EMIT infoMessage(xi18nd("@info:status", "Pulling branch %1 from %2...", dialog.remoteBranch(), 0699 dialog.source())); 0700 0701 m_command = QStringLiteral("pull"); 0702 m_pendingOperation = true; 0703 m_process.start(QStringLiteral("git"), {QStringLiteral("pull"), dialog.source(), dialog.remoteBranch()}); 0704 } 0705 } 0706 0707 void FileViewGitPlugin::slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus) 0708 { 0709 m_pendingOperation = false; 0710 0711 QString message; 0712 if (m_command == QLatin1String("push")) { //output parsing for push 0713 message = parsePushOutput(); 0714 m_command = QString(); 0715 } 0716 if (m_command == QLatin1String("pull")) { 0717 message = parsePullOutput(); 0718 m_command = QString(); 0719 } 0720 0721 if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { 0722 Q_EMIT errorMessage(message.isNull() ? m_errorMsg : message); 0723 } else if (m_contextItems.isEmpty()) { 0724 Q_EMIT operationCompletedMessage(message.isNull() ? m_operationCompletedMsg : message); 0725 Q_EMIT itemVersionsChanged(); 0726 } else { 0727 startGitCommandProcess(); 0728 } 0729 } 0730 0731 void FileViewGitPlugin::slotOperationError() 0732 { 0733 // don't do any operation on other items anymore 0734 m_contextItems.clear(); 0735 m_pendingOperation = false; 0736 0737 Q_EMIT errorMessage(m_errorMsg); 0738 } 0739 0740 QString FileViewGitPlugin::parsePushOutput() 0741 { 0742 m_process.setReadChannel(QProcess::StandardError); 0743 QString message; 0744 char buffer[256]; 0745 while (m_process.readLine(buffer, sizeof(buffer)) > 0) { 0746 const QString line = QString::fromLocal8Bit(buffer); 0747 if (line.contains(QLatin1String("->")) || (line.contains(QLatin1String("fatal")) && message.isNull())) { 0748 message = line.trimmed(); 0749 } 0750 if (line.contains(QLatin1String("Everything up-to-date")) && message.isNull()) { 0751 message = xi18nd("@info:status", "Branch is already up-to-date."); 0752 } 0753 } 0754 return message; 0755 } 0756 0757 QString FileViewGitPlugin::parsePullOutput() 0758 { 0759 char buffer[256]; 0760 while (m_process.readLine(buffer, sizeof(buffer)) > 0) { 0761 const QString line = QString::fromLocal8Bit(buffer); 0762 if (line.contains(QLatin1String("Already up-to-date"))) { 0763 return xi18nd("@info:status", "Branch is already up-to-date."); 0764 } 0765 if (line.contains(QLatin1String("CONFLICT"))) { 0766 Q_EMIT itemVersionsChanged(); 0767 return xi18nd("@info:status", "Merge conflicts occurred. Fix them and commit the result."); 0768 } 0769 } 0770 return QString(); 0771 } 0772 0773 void FileViewGitPlugin::execGitCommand(const QString& gitCommand, 0774 const QStringList& arguments, 0775 const QString& infoMsg, 0776 const QString& errorMsg, 0777 const QString& operationCompletedMsg) 0778 { 0779 Q_EMIT infoMessage(infoMsg); 0780 0781 m_command = gitCommand; 0782 m_arguments = arguments; 0783 m_errorMsg = errorMsg; 0784 m_operationCompletedMsg = operationCompletedMsg; 0785 0786 startGitCommandProcess(); 0787 } 0788 0789 0790 void FileViewGitPlugin::startGitCommandProcess() 0791 { 0792 Q_ASSERT(!m_contextItems.isEmpty()); 0793 Q_ASSERT(m_process.state() == QProcess::NotRunning); 0794 m_pendingOperation = true; 0795 0796 const KFileItem item = m_contextItems.takeLast(); 0797 m_process.setWorkingDirectory(m_contextDir); 0798 QStringList arguments; 0799 arguments << m_command; 0800 arguments << m_arguments; 0801 //force explicitly selected files but no files in selected directories 0802 if (m_command == QLatin1String("add") && !item.isDir()){ 0803 arguments<< QStringLiteral("-f"); 0804 } 0805 arguments << item.url().fileName(); 0806 m_process.start(QStringLiteral("git"), arguments); 0807 // the remaining items of m_contextItems will be executed 0808 // after the process has finished (see slotOperationFinished()) 0809 } 0810 0811 #include "fileviewgitplugin.moc" 0812 0813 #include "moc_fileviewgitplugin.cpp"