/*************************************************************************** * Copyright (C) 2011 by Vishesh Yadav * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "fileviewhgplugin.h" #include "hgconfig.h" #include "configdialog.h" #include "renamedialog.h" #include "commitdialog.h" #include "branchdialog.h" #include "tagdialog.h" #include "updatedialog.h" #include "clonedialog.h" #include "createdialog.h" #include "pushdialog.h" #include "pulldialog.h" #include "mergedialog.h" #include "bundledialog.h" #include "exportdialog.h" #include "importdialog.h" #include "servedialog.h" #include "backoutdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY(FileViewHgPluginFactory, registerPlugin();) K_EXPORT_PLUGIN(FileViewHgPluginFactory("fileviewhgplugin")) //TODO: Build a proper status signal system to sync HgWrapper/Dialgs with this //TODO: Show error messages and set their message approproately(hg output) //TODO: Use i18nc rather thn i18c throughout plugin FileViewHgPlugin::FileViewHgPlugin(QObject *parent, const QList &args): KVersionControlPlugin(parent), m_mainContextMenu(0), m_addAction(0), m_removeAction(0), m_renameAction(0), m_commitAction(0), m_branchAction(0), m_tagAction(0), m_updateAction(0), m_cloneAction(0), m_createAction(0), m_globalConfigAction(0), m_repoConfigAction(0), m_pushAction(0), m_pullAction(0), m_revertAction(0), m_revertAllAction(0), m_rollbackAction(0), m_mergeAction(0), m_bundleAction(0), m_exportAction(0), m_unbundleAction(0), m_importAction(0), m_diffAction(0), m_serveAction(0), m_backoutAction(0), m_hgWrapper(0), m_retrievalHgw(0) { Q_UNUSED(args); m_addAction = new KAction(this); m_addAction->setIcon(KIcon("list-add")); m_addAction->setText(i18nc("@action:inmenu", "Hg Add")); connect(m_addAction, SIGNAL(triggered()), this, SLOT(addFiles())); m_removeAction = new KAction(this); m_removeAction->setIcon(KIcon("list-remove")); m_removeAction->setText(i18nc("@action:inmenu", "Hg Remove")); connect(m_removeAction, SIGNAL(triggered()), this, SLOT(removeFiles())); m_renameAction = new KAction(this); m_renameAction->setIcon(KIcon("list-rename")); m_renameAction->setText(i18nc("@action:inmenu", "Hg Rename")); connect(m_renameAction, SIGNAL(triggered()), this, SLOT(renameFile())); m_commitAction = new KAction(this); m_commitAction->setIcon(KIcon("svn-commit")); m_commitAction->setText(i18nc("@action:inmenu", "Hg Commit")); connect(m_commitAction, SIGNAL(triggered()), this, SLOT(commit())); m_tagAction = new KAction(this); m_tagAction->setIcon(KIcon("bookmark-new")); m_tagAction->setText(i18nc("@action:inmenu", "Hg Tag")); connect(m_tagAction, SIGNAL(triggered()), this, SLOT(tag())); m_branchAction = new KAction(this); m_branchAction->setIcon(KIcon("bookmark-new")); m_branchAction->setText(i18nc("@action:inmenu", "Hg Branch")); connect(m_branchAction, SIGNAL(triggered()), this, SLOT(branch())); m_cloneAction = new KAction(this); m_cloneAction->setIcon(KIcon("hg-clone")); m_cloneAction->setText(i18nc("@action:inmenu", "Hg Clone")); connect(m_cloneAction, SIGNAL(triggered()), this, SLOT(clone())); m_createAction = new KAction(this); m_createAction->setIcon(KIcon("hg-create")); m_createAction->setText(i18nc("@action:inmenu", "Hg Init")); connect(m_createAction, SIGNAL(triggered()), this, SLOT(create())); m_updateAction = new KAction(this); m_updateAction->setIcon(KIcon("svn-update")); m_updateAction->setText(i18nc("@action:inmenu", "Hg Update")); connect(m_updateAction, SIGNAL(triggered()), this, SLOT(update())); m_globalConfigAction = new KAction(this); m_globalConfigAction->setIcon(KIcon("hg-config")); m_globalConfigAction->setText(i18nc("@action:inmenu", "Hg Global Config")); connect(m_globalConfigAction, SIGNAL(triggered()), this, SLOT(global_config())); m_repoConfigAction = new KAction(this); m_repoConfigAction->setIcon(KIcon("hg-config")); m_repoConfigAction->setText(i18nc("@action:inmenu", "Hg Repository Config")); connect(m_repoConfigAction, SIGNAL(triggered()), this, SLOT(repo_config())); m_pushAction = new KAction(this); m_pushAction->setIcon(KIcon("git-push")); m_pushAction->setText(i18nc("@action:inmenu", "Hg Push")); connect(m_pushAction, SIGNAL(triggered()), this, SLOT(push())); m_pullAction = new KAction(this); m_pullAction->setIcon(KIcon("git-pull")); m_pullAction->setText(i18nc("@action:inmenu", "Hg Pull")); connect(m_pullAction, SIGNAL(triggered()), this, SLOT(pull())); m_revertAction = new KAction(this); m_revertAction->setIcon(KIcon("hg-revert")); m_revertAction->setText(i18nc("@action:inmenu", "Hg Revert")); connect(m_revertAction, SIGNAL(triggered()), this, SLOT(revert())); m_revertAllAction = new KAction(this); m_revertAllAction->setIcon(KIcon("hg-revert")); m_revertAllAction->setText(i18nc("@action:inmenu", "Hg Revert All")); connect(m_revertAllAction, SIGNAL(triggered()), this, SLOT(revertAll())); m_rollbackAction = new KAction(this); m_rollbackAction->setIcon(KIcon("hg-rollback")); m_rollbackAction->setText(i18nc("@action:inmenu", "Hg Rollback")); connect(m_rollbackAction, SIGNAL(triggered()), this, SLOT(rollback())); m_mergeAction = new KAction(this); m_mergeAction->setIcon(KIcon("hg-merge")); m_mergeAction->setText(i18nc("@action:inmenu", "Hg Merge")); connect(m_mergeAction, SIGNAL(triggered()), this, SLOT(merge())); m_bundleAction = new KAction(this); m_bundleAction->setIcon(KIcon("hg-bundle")); m_bundleAction->setText(i18nc("@action:inmenu", "Hg Bundle")); connect(m_bundleAction, SIGNAL(triggered()), this, SLOT(bundle())); m_exportAction = new KAction(this); m_exportAction->setIcon(KIcon("hg-export")); m_exportAction->setText(i18nc("@action:inmenu", "Hg Export")); connect(m_exportAction, SIGNAL(triggered()), this, SLOT(exportChangesets())); m_importAction = new KAction(this); m_importAction->setIcon(KIcon("hg-import")); m_importAction->setText(i18nc("@action:inmenu", "Hg Import")); connect(m_importAction, SIGNAL(triggered()), this, SLOT(importChangesets())); m_unbundleAction = new KAction(this); m_unbundleAction->setIcon(KIcon("hg-unbundle")); m_unbundleAction->setText(i18nc("@action:inmenu", "Hg Unbundle")); connect(m_unbundleAction, SIGNAL(triggered()), this, SLOT(unbundle())); m_serveAction = new KAction(this); m_serveAction->setIcon(KIcon("hg-serve")); m_serveAction->setText(i18nc("@action:inmenu", "Hg Serve")); connect(m_serveAction, SIGNAL(triggered()), this, SLOT(serve())); m_backoutAction = new KAction(this); m_backoutAction->setIcon(KIcon("hg-backout")); m_backoutAction->setText(i18nc("@action:inmenu", "Hg Backout")); connect(m_backoutAction, SIGNAL(triggered()), this, SLOT(backout())); m_diffAction = new KAction(this); m_diffAction->setIcon(KIcon("hg-diff")); m_diffAction->setText(i18nc("@action:inmenu", "Hg Diff")); connect(m_diffAction, SIGNAL(triggered()), this, SLOT(diff())); /* Submenu to make the main menu less cluttered */ m_mainContextMenu = new KMenu; m_mainContextMenu->addAction(m_updateAction); m_mainContextMenu->addAction(m_branchAction); m_mainContextMenu->addAction(m_tagAction); m_mainContextMenu->addAction(m_mergeAction); m_mainContextMenu->addAction(m_revertAllAction); m_mainContextMenu->addAction(m_rollbackAction); m_mainContextMenu->addAction(m_backoutAction); m_mainContextMenu->addAction(m_bundleAction); m_mainContextMenu->addAction(m_unbundleAction); m_mainContextMenu->addAction(m_exportAction); m_mainContextMenu->addAction(m_importAction); m_mainContextMenu->addAction(m_serveAction); m_mainContextMenu->addAction(m_globalConfigAction); m_mainContextMenu->addAction(m_repoConfigAction); m_menuAction = new KAction(this); m_menuAction->setIcon(KIcon("hg-main")); m_menuAction->setText(i18nc("@action:inmenu", "Mercurial")); m_menuAction->setMenu(m_mainContextMenu); } FileViewHgPlugin::~FileViewHgPlugin() { } void FileViewHgPlugin::createHgWrapper() const { static bool created = false; if (created && m_hgWrapper != 0) { return; } created = true; m_hgWrapper = HgWrapper::instance(); connect(m_hgWrapper, SIGNAL(primaryOperationFinished(int, QProcess::ExitStatus)), this, SLOT(slotOperationCompleted(int, QProcess::ExitStatus))); connect(m_hgWrapper, SIGNAL(primaryOperationError(QProcess::ProcessError)), this, SLOT(slotOperationError())); } QString FileViewHgPlugin::fileName() const { return QLatin1String(".hg"); } bool FileViewHgPlugin::beginRetrieval(const QString &directory) { clearMessages(); m_currentDir = directory; m_versionInfoHash.clear(); //createHgWrapper(); //m_hgWrapper->setCurrentDir(directory); //m_hgWrapper->getItemVersions(m_versionInfoHash); if (m_retrievalHgw == 0) { m_retrievalHgw = new HgWrapper; } m_retrievalHgw->setCurrentDir(directory); m_retrievalHgw->getItemVersions(m_versionInfoHash); return true; } void FileViewHgPlugin::endRetrieval() { } KVersionControlPlugin::ItemVersion FileViewHgPlugin::itemVersion(const KFileItem &item) const { //FIXME: When folder is empty or all files within untracked. const QString itemUrl = item.localPath(); if (item.isDir()) { QHash::const_iterator it = m_versionInfoHash.constBegin(); while (it != m_versionInfoHash.constEnd()) { if (it.key().startsWith(itemUrl)) { const ItemVersion state = m_versionInfoHash.value(it.key()); if (state == LocallyModifiedVersion || state == AddedVersion || state == RemovedVersion) { return LocallyModifiedVersion; } } ++it; } // Making folders with all files within untracked 'Unversioned' // will disable the context menu there. Will enable recursive // add however. QDir dir(item.localPath()); QStringList filesInside = dir.entryList(); foreach (const QString &fileName, filesInside) { if (fileName == "." || fileName == ".." ) { continue; } KUrl tempUrl(dir.absoluteFilePath(fileName)); KFileItem tempFileItem(KFileItem::Unknown, KFileItem::Unknown, tempUrl); if (itemVersion(tempFileItem) == NormalVersion) { return NormalVersion; } } return UnversionedVersion; } if (m_versionInfoHash.contains(itemUrl)) { return m_versionInfoHash.value(itemUrl); } return NormalVersion; } QList FileViewHgPlugin::actions(const KFileItemList &items) const { //TODO: Make it work with universal context menu when imlpemented // in dolphin kDebug() << items.count(); if (items.count() == 1 && items.first().isDir()) { return directoryContextMenu(m_currentDir); } else { return itemContextMenu(items); } return QList(); } QList FileViewHgPlugin::universalContextMenuActions(const QString &directory) const { QList result; m_universalCurrentDirectory = directory; result.append(m_createAction); result.append(m_cloneAction); return result; } QList FileViewHgPlugin::itemContextMenu(const KFileItemList &items) const { Q_ASSERT(!items.isEmpty()); clearMessages(); createHgWrapper(); m_hgWrapper->setCurrentDir(m_currentDir); if (!m_hgWrapper->isBusy()) { m_contextItems.clear(); foreach (const KFileItem &item, items) { m_contextItems.append(item); } //see which actions should be enabled int versionedCount = 0; int addableCount = 0; int revertableCount = 0; foreach (const KFileItem &item, items) { const ItemVersion state = itemVersion(item); if (state != UnversionedVersion && state != RemovedVersion) { ++versionedCount; } if (state == UnversionedVersion || state == LocallyModifiedUnstagedVersion) { ++addableCount; } if (state == LocallyModifiedVersion || state == AddedVersion || state == RemovedVersion) { ++revertableCount; } } m_addAction->setEnabled(addableCount == items.count()); m_removeAction->setEnabled(versionedCount == items.count()); m_revertAction->setEnabled(revertableCount == items.count()); m_diffAction->setEnabled(revertableCount == items.count() && items.size() == 1); m_renameAction->setEnabled(items.size() == 1 && itemVersion(items.first()) != UnversionedVersion); } else { m_addAction->setEnabled(false); m_removeAction->setEnabled(false); m_renameAction->setEnabled(false); m_revertAction->setEnabled(false); m_diffAction->setEnabled(false); } QList actions; actions.append(m_addAction); actions.append(m_removeAction); actions.append(m_renameAction); actions.append(m_revertAction); actions.append(m_diffAction); return actions; } QList FileViewHgPlugin::directoryContextMenu(const QString &directory) const { QList actions; clearMessages(); createHgWrapper(); m_hgWrapper->setCurrentDir(directory); if (!m_hgWrapper->isBusy()) { actions.append(m_commitAction); } actions.append(m_pushAction); actions.append(m_pullAction); actions.append(m_diffAction); actions.append(m_menuAction); return actions; } void FileViewHgPlugin::addFiles() { Q_ASSERT(!m_contextItems.isEmpty()); QString infoMsg = i18nc("@info:status", "Adding files to Hg repository..."); m_errorMsg = i18nc("@info:status", "Adding files to Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Added files to Hg repository."); emit infoMessage(infoMsg); m_hgWrapper->addFiles(m_contextItems); } void FileViewHgPlugin::removeFiles() { Q_ASSERT(!m_contextItems.isEmpty()); int answer = KMessageBox::questionYesNo(0, i18nc("@message:yesorno", "Would you like to remove selected files " "from the repository?")); if (answer == KMessageBox::No) { return; } QString infoMsg = i18nc("@info:status", "Removing files from Hg repository..."); m_errorMsg = i18nc("@info:status", "Removing files from Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Removed files from Hg repository."); emit infoMessage(infoMsg); m_hgWrapper->removeFiles(m_contextItems); } void FileViewHgPlugin::renameFile() { Q_ASSERT(m_contextItems.size() == 1); m_errorMsg = i18nc("@info:status", "Renaming of file in Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Renamed file in Hg repository successfully."); emit infoMessage(i18nc("@info:status", "Renaming file in Hg repository.")); HgRenameDialog dialog(m_contextItems.first()); dialog.exec(); m_contextItems.clear(); } void FileViewHgPlugin::commit() { if (m_hgWrapper->isWorkingDirectoryClean()) { KMessageBox::information(0, i18nc("@message", "No changes for commit!")); return; } //FIXME: Disable emitting of status messages when executing sub tasks. m_errorMsg = i18nc("@info:status", "Commit to Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Committed to Hg repository."); emit infoMessage(i18nc("@info:status", "Commit Hg repository.")); HgCommitDialog dialog; if (dialog.exec() == QDialog::Accepted) { emit itemVersionsChanged(); }; } void FileViewHgPlugin::tag() { m_errorMsg = i18nc("@info:status", "Tag operation in Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Tagging operation in Hg repository is successful."); emit infoMessage(i18nc("@info:status", "Tagging operation in Hg repository.")); HgTagDialog dialog; dialog.exec(); } void FileViewHgPlugin::update() { m_errorMsg = i18nc("@info:status", "Update of Hg working directory failed."); m_operationCompletedMsg = i18nc("@info:status", "Update of Hg working directory is successful."); emit infoMessage(i18nc("@info:status", "Updating Hg working directory.")); HgUpdateDialog dialog; dialog.exec(); } void FileViewHgPlugin::branch() { m_errorMsg = i18nc("@info:status", "Branch operation on Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Branch operation on Hg repository completed successfully."); emit infoMessage(i18nc("@info:status", "Branch operation on Hg repository.")); HgBranchDialog dialog; dialog.exec(); } void FileViewHgPlugin::clone() { clearMessages(); HgCloneDialog dialog(m_universalCurrentDirectory); dialog.exec(); } void FileViewHgPlugin::create() { clearMessages(); HgCreateDialog dialog(m_universalCurrentDirectory); dialog.exec(); } void FileViewHgPlugin::global_config() { clearMessages(); HgConfigDialog diag(HgConfig::GlobalConfig); diag.exec(); } void FileViewHgPlugin::repo_config() { clearMessages(); HgConfigDialog diag(HgConfig::RepoConfig); diag.exec(); } void FileViewHgPlugin::push() { clearMessages(); HgPushDialog diag; diag.exec(); } void FileViewHgPlugin::pull() { clearMessages(); HgPullDialog diag; diag.exec(); } void FileViewHgPlugin::merge() { clearMessages(); HgMergeDialog diag; diag.exec(); } void FileViewHgPlugin::bundle() { clearMessages(); HgBundleDialog diag; diag.exec(); } void FileViewHgPlugin::unbundle() { clearMessages(); QString bundle = KFileDialog::getOpenFileName(); if (bundle.isEmpty()) { return; } QStringList args; args << bundle; if (m_hgWrapper->executeCommandTillFinished(QLatin1String("unbundle"), args)) { } else { KMessageBox::error(0, m_hgWrapper->readAllStandardError()); } } void FileViewHgPlugin::importChangesets() { clearMessages(); HgImportDialog diag; diag.exec(); } void FileViewHgPlugin::exportChangesets() { clearMessages(); HgExportDialog diag; diag.exec(); } void FileViewHgPlugin::revert() { clearMessages(); int answer = KMessageBox::questionYesNo(0, i18nc("@message:yesorno", "Would you like to revert changes " "made to selected files?")); if (answer == KMessageBox::No) { return; } QString infoMsg = i18nc("@info:status", "Reverting files in Hg repository..."); m_errorMsg = i18nc("@info:status", "Reverting files in Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Reverting files in Hg repository completed successfully."); emit infoMessage(infoMsg); m_hgWrapper->revert(m_contextItems); } void FileViewHgPlugin::revertAll() { int answer = KMessageBox::questionYesNo(0, i18nc("@message:yesorno", "Would you like to revert all changes " "made to current working directory?")); if (answer == KMessageBox::No) { return; } QString infoMsg = i18nc("@info:status", "Reverting files in Hg repository..."); m_errorMsg = i18nc("@info:status", "Reverting files in Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Reverting files in Hg repository completed successfully."); emit infoMessage(infoMsg); m_hgWrapper->revertAll(); } void FileViewHgPlugin::diff() { QString infoMsg = i18nc("@info:status", "Generating diff for Hg repository..."); m_errorMsg = i18nc("@info:status", "Could not get Hg repository diff."); m_operationCompletedMsg = i18nc("@info:status", "Generated Hg diff successfully."); emit infoMessage(infoMsg); QStringList args; args << QLatin1String("--config"); args << QLatin1String("extensions.hgext.extdiff="); args << QLatin1String("-p"); args << this->visualDiffExecPath(); if (m_contextItems.length() == 1) { args << m_contextItems.takeFirst().localPath(); } m_hgWrapper->executeCommand(QLatin1String("extdiff"), args); } void FileViewHgPlugin::serve() { clearMessages(); HgServeDialog diag; diag.exec(); } void FileViewHgPlugin::backout() { clearMessages(); m_hgWrapper = HgWrapper::instance(); if (!m_hgWrapper->isWorkingDirectoryClean()) { KMessageBox::error(0, i18nc("@message:error", "abort: Uncommitted changes in working directory!")); return; } HgBackoutDialog diag; diag.exec(); } void FileViewHgPlugin::rollback() { // execute a dry run rollback first to see if there is anything to // be rolled back, or check what will be rolled back if (!m_hgWrapper->rollback(true)) { KMessageBox::error(0, i18nc("@info:message", "No rollback " "information available!")); return; } // get what will be rolled back QString lastTransaction = m_hgWrapper->readAllStandardOutput(); int cutOfFrom = lastTransaction.indexOf(QRegExp("\\d")); lastTransaction = lastTransaction.mid(cutOfFrom); // ask int answer = KMessageBox::questionYesNo(0, i18nc("@message:yesorno", "Would you like to rollback last transaction?") + "\nrevision: " + lastTransaction); if (answer == KMessageBox::No) { return; } QString infoMsg = i18nc("@info:status", "Executing Rollback Hg repository..."); m_errorMsg = i18nc("@info:status", "Rollback of Hg repository failed."); m_operationCompletedMsg = i18nc("@info:status", "Rollback of Hg repository completed successfully."); emit infoMessage(infoMsg); m_hgWrapper->rollback(); KMessageBox::information(0, m_hgWrapper->readAllStandardOutput()); emit itemVersionsChanged(); } void FileViewHgPlugin::slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus) { if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { emit errorMessage(m_errorMsg); } else { m_contextItems.clear(); emit operationCompletedMessage(m_operationCompletedMsg); emit itemVersionsChanged(); } } void FileViewHgPlugin::slotOperationError() { m_contextItems.clear(); emit errorMessage(m_errorMsg); } void FileViewHgPlugin::clearMessages() const { m_operationCompletedMsg.clear(); m_errorMsg.clear(); } QString FileViewHgPlugin::visualDiffExecPath() { KUrl url = KUrl::fromPath(QDir::homePath()); url.addPath(".dolphin-hg"); KConfig config(url.path(), KConfig::SimpleConfig); KConfigGroup group(&config, QLatin1String("diff")); QString result = group.readEntry(QLatin1String("exec"), QString()).trimmed(); if (result.length() > 0) { return result; } KService::List services = KMimeTypeTrader::self()->query("text/x-diff"); return services.first()->exec().split(' ').takeFirst(); } #include "moc_fileviewhgplugin.cpp"