From 93b156e58539f49b30f535bc9942d0c7fad0c097 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint <Friedemann.Kleint@nokia.com> Date: Fri, 12 Mar 2010 15:54:09 +0100 Subject: [PATCH] VCS[git]: Add support for cleaning a repository. Present user with a checkable list of files to be cleaned (add reusable dialog to VCSBase module). --- src/plugins/git/gitclient.cpp | 24 +++ src/plugins/git/gitclient.h | 1 + src/plugins/git/gitplugin.cpp | 45 +++++ src/plugins/git/gitplugin.h | 2 + src/plugins/vcsbase/cleandialog.cpp | 264 ++++++++++++++++++++++++++++ src/plugins/vcsbase/cleandialog.h | 74 ++++++++ src/plugins/vcsbase/cleandialog.ui | 73 ++++++++ src/plugins/vcsbase/vcsbase.pro | 9 +- 8 files changed, 489 insertions(+), 3 deletions(-) create mode 100644 src/plugins/vcsbase/cleandialog.cpp create mode 100644 src/plugins/vcsbase/cleandialog.h create mode 100644 src/plugins/vcsbase/cleandialog.ui diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index 97460f867dd..b7ad77b1d53 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -949,6 +949,30 @@ bool GitClient::synchronousShow(const QString &workingDirectory, const QString & return true; } +// Retrieve list of files to be cleaned +bool GitClient::synchronousCleanList(const QString &workingDirectory, + QStringList *files, QString *errorMessage) +{ + if (Git::Constants::debug) + qDebug() << Q_FUNC_INFO << workingDirectory; + files->clear(); + QStringList args; + args << QLatin1String("clean") << QLatin1String("--dry-run") << QLatin1String("-dxf"); + QByteArray outputText; + QByteArray errorText; + const bool rc = synchronousGit(workingDirectory, args, &outputText, &errorText); + if (!rc) { + *errorMessage = tr("Unable to run clean --dry-run: %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText)); + return false; + } + // Filter files that git would remove + const QString prefix = QLatin1String("Would remove "); + foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText)) + if (line.startsWith(prefix)) + files->push_back(line.mid(prefix.size())); + return true; +} + // Factory function to create an asynchronous command GitCommand *GitClient::createCommand(const QString &workingDirectory, VCSBase::VCSBaseEditor* editor, diff --git a/src/plugins/git/gitclient.h b/src/plugins/git/gitclient.h index 65ea8e295a1..42b2e1bb1c8 100644 --- a/src/plugins/git/gitclient.h +++ b/src/plugins/git/gitclient.h @@ -105,6 +105,7 @@ public: bool synchronousReset(const QString &workingDirectory, const QStringList &files = QStringList(), QString *errorMessage = 0); + bool synchronousCleanList(const QString &workingDirectory, QStringList *files, QString *errorMessage); bool synchronousInit(const QString &workingDirectory); bool synchronousCheckoutFiles(const QString &workingDirectory, QStringList files = QStringList(), diff --git a/src/plugins/git/gitplugin.cpp b/src/plugins/git/gitplugin.cpp index 9ab4518f73d..be0401aead0 100644 --- a/src/plugins/git/gitplugin.cpp +++ b/src/plugins/git/gitplugin.cpp @@ -56,6 +56,7 @@ #include <vcsbase/vcsbaseeditor.h> #include <vcsbase/basevcssubmiteditorfactory.h> #include <vcsbase/vcsbaseoutputwindow.h> +#include <vcsbase/cleandialog.h> #include <locator/commandlocator.h> #include <QtCore/QDebug> @@ -132,6 +133,7 @@ GitPlugin::GitPlugin() : m_commitAction(0), m_pullAction(0), m_pushAction(0), + m_cleanAction(0), m_submitCurrentAction(0), m_diffSelectedFilesAction(0), m_undoAction(0), @@ -332,6 +334,12 @@ bool GitPlugin::initialize(const QStringList &arguments, QString *errorMessage) connect(m_createRepositoryAction, SIGNAL(triggered()), this, SLOT(createRepository())); gitContainer->addAction(command); + m_cleanAction = new QAction(tr("Clean Repository..."), this); + command = actionManager->registerAction(m_cleanAction, "Git.CleanRepository", globalcontext); + connect(m_cleanAction, SIGNAL(triggered()), this, SLOT(cleanRepository())); + gitContainer->addAction(command); + m_commandLocator->appendCommand(command); + gitContainer->addAction(createSeparator(actionManager, globalcontext, QLatin1String("Git.Sep.Global"), this)); m_stashSnapshotAction = new QAction(tr("Stash snapshot..."), this); @@ -680,6 +688,42 @@ void GitPlugin::push() m_gitClient->push(state.topLevel()); } +void GitPlugin::cleanRepository() +{ + const VCSBase::VCSBasePluginState state = currentState(); + QTC_ASSERT(state.hasTopLevel(), return); + + // Find files to be deleted + QString errorMessage; + QStringList files; + QApplication::setOverrideCursor(Qt::WaitCursor); + const bool gotFiles = m_gitClient->synchronousCleanList(state.topLevel(), &files, &errorMessage); + QApplication::restoreOverrideCursor(); + + QWidget *parent = Core::ICore::instance()->mainWindow(); + if (!gotFiles) { + QMessageBox::warning(parent, tr("Unable to retrieve file list"), + errorMessage); + return; + } + if (files.isEmpty()) { + QMessageBox::information(parent, tr("Repository clean"), + tr("The repository is clean.")); + return; + } + // Clean the trailing slash of directories + const QChar slash = QLatin1Char('/'); + const QStringList::iterator end = files.end(); + for (QStringList::iterator it = files.begin(); it != end; ++it) + if (it->endsWith(slash)) + it->truncate(it->size() - 1); + + // Show in dialog + VCSBase::CleanDialog dialog(parent); + dialog.setFileList(state.topLevel(), files); + dialog.exec(); +} + void GitPlugin::stash() { // Simple stash without prompt, reset repo. @@ -780,6 +824,7 @@ void GitPlugin::updateActions(VCSBase::VCSBasePlugin::ActionState as) m_logRepositoryAction->setEnabled(repositoryEnabled); m_undoRepositoryAction->setEnabled(repositoryEnabled); m_pushAction->setEnabled(repositoryEnabled); + m_cleanAction->setEnabled(repositoryEnabled); // Prompts for repo. m_showAction->setEnabled(true); diff --git a/src/plugins/git/gitplugin.h b/src/plugins/git/gitplugin.h index 746cf60ab49..50072d497b4 100644 --- a/src/plugins/git/gitplugin.h +++ b/src/plugins/git/gitplugin.h @@ -105,6 +105,7 @@ private slots: void undoRepositoryChanges(); void stageFile(); void unstageFile(); + void cleanRepository(); void showCommit(); void startCommit(); @@ -146,6 +147,7 @@ private: QAction *m_commitAction; QAction *m_pullAction; QAction *m_pushAction; + QAction *m_cleanAction; QAction *m_submitCurrentAction; QAction *m_diffSelectedFilesAction; diff --git a/src/plugins/vcsbase/cleandialog.cpp b/src/plugins/vcsbase/cleandialog.cpp new file mode 100644 index 00000000000..a8ed8a02309 --- /dev/null +++ b/src/plugins/vcsbase/cleandialog.cpp @@ -0,0 +1,264 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#include "cleandialog.h" +#include "ui_cleandialog.h" +#include "vcsbaseoutputwindow.h" + +#include <coreplugin/editormanager/editormanager.h> +#include <coreplugin/icore.h> +#include <coreplugin/progressmanager/progressmanager.h> + +#include <QtGui/QStandardItemModel> +#include <QtGui/QMessageBox> +#include <QtGui/QApplication> +#include <QtGui/QStyle> +#include <QtGui/QIcon> + +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QDebug> +#include <QtCore/QDateTime> +#include <QtCore/QFuture> +#include <QtCore/QtConcurrentRun> + +enum { nameColumn, columnCount }; +enum { fileNameRole = Qt::UserRole, isDirectoryRole = Qt::UserRole + 1 }; + +// Helper for recursively removing files. +static void removeFileRecursion(const QFileInfo &f, QString *errorMessage) +{ + // The version control system might list files/directory in arbitrary + // order, causing files to be removed from parent directories. + if (!f.exists()) + return; + if (f.isDir()) { + const QDir dir(f.absoluteFilePath()); + foreach(const QFileInfo &fi, dir.entryInfoList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::Hidden)) + removeFileRecursion(fi, errorMessage); + QDir parent = f.absoluteDir(); + if (!parent.rmdir(f.fileName())) + errorMessage->append(VCSBase::CleanDialog::tr("The directory %1 could not be deleted.").arg(f.absoluteFilePath())); + return; + } + if (!QFile::remove(f.absoluteFilePath())) { + if (!errorMessage->isEmpty()) + errorMessage->append(QLatin1Char('\n')); + errorMessage->append(VCSBase::CleanDialog::tr("The file %1 could not be deleted.").arg(f.absoluteFilePath())); + } +} + +namespace VCSBase { + +// A QFuture task for cleaning files in the background. +// Emits error signal if not all files can be deleted. +class CleanFilesTask : public QObject { + Q_OBJECT +public: + explicit CleanFilesTask(const QString &repository, const QStringList &files); + + void run(); + +signals: + void error(const QString &e); + +private: + const QString m_repository; + const QStringList m_files; + + QString m_errorMessage; +}; + +CleanFilesTask::CleanFilesTask(const QString &repository, const QStringList &files) : + m_repository(repository), m_files(files) +{ +} + +void CleanFilesTask::run() +{ + foreach(const QString &name, m_files) + removeFileRecursion(QFileInfo(name), &m_errorMessage); + if (!m_errorMessage.isEmpty()) { + // Format and emit error. + const QString msg = CleanDialog::tr("There were errors when cleaning the repository %1:").arg(m_repository); + m_errorMessage.insert(0, QLatin1Char('\n')); + m_errorMessage.insert(0, msg); + emit error(m_errorMessage); + } + // Run in background, need to delete ourselves + this->deleteLater(); +} + +// ---------------- CleanDialogPrivate ---------------- +struct CleanDialogPrivate { + CleanDialogPrivate(); + + Ui::CleanDialog ui; + QStandardItemModel *m_filesModel; + QString m_workingDirectory; +}; + +CleanDialogPrivate::CleanDialogPrivate() : m_filesModel(new QStandardItemModel(0, columnCount)) +{ +} + +CleanDialog::CleanDialog(QWidget *parent) : + QDialog(parent), + d(new CleanDialogPrivate) +{ + setModal(true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + d->ui.setupUi(this); + d->ui.buttonBox->addButton(tr("Delete..."), QDialogButtonBox::AcceptRole); + + d->m_filesModel->setHorizontalHeaderLabels(QStringList(tr("Name"))); + d->ui.filesTreeView->setModel(d->m_filesModel); + d->ui.filesTreeView->setUniformRowHeights(true); + d->ui.filesTreeView->setSelectionMode(QAbstractItemView::NoSelection); + d->ui.filesTreeView->setAllColumnsShowFocus(true); + d->ui.filesTreeView->setRootIsDecorated(false); + connect(d->ui.filesTreeView, SIGNAL(doubleClicked(QModelIndex)), + this, SLOT(slotDoubleClicked(QModelIndex))); +} + +CleanDialog::~CleanDialog() +{ + delete d; +} + +void CleanDialog::setFileList(const QString &workingDirectory, const QStringList &l) +{ + d->m_workingDirectory = workingDirectory; + d->ui.groupBox->setTitle(tr("Repository: %1").arg(workingDirectory)); + if (const int oldRowCount = d->m_filesModel->rowCount()) + d->m_filesModel->removeRows(0, oldRowCount); + + QStyle *style = QApplication::style(); + const QIcon folderIcon = style->standardIcon(QStyle::SP_DirIcon); + const QIcon fileIcon = style->standardIcon(QStyle::SP_FileIcon); + const QString diffSuffix = QLatin1String(".diff"); + const QString patchSuffix = QLatin1String(".patch"); + const QChar slash = QLatin1Char('/'); + // Do not initially check patches for deletion. + foreach(const QString &fileName, l) { + const QFileInfo fi(workingDirectory + slash + fileName); + const bool isDir = fi.isDir(); + QStandardItem *nameItem = new QStandardItem(QDir::toNativeSeparators(fileName)); + nameItem->setFlags(Qt::ItemIsUserCheckable|Qt::ItemIsEnabled); + nameItem->setIcon(isDir ? folderIcon : fileIcon); + const bool isPatch = !isDir && (fileName.endsWith(diffSuffix) + || fileName.endsWith(patchSuffix)); + nameItem->setCheckable(true); + nameItem->setCheckState(isPatch ? Qt::Unchecked : Qt::Checked); + nameItem->setData(QVariant(fi.absoluteFilePath()), fileNameRole); + nameItem->setData(QVariant(isDir), isDirectoryRole); + // Tooltip with size information + if (fi.isFile()) { + const QString lastModified = fi.lastModified().toString(Qt::DefaultLocaleShortDate); + nameItem->setToolTip(tr("%1 bytes, last modified %2") + .arg(fi.size()).arg(lastModified)); + } + d->m_filesModel->appendRow(nameItem); + } + + for (int c = 0; c < d->m_filesModel->columnCount(); c++) + d->ui.filesTreeView->resizeColumnToContents(c); +} + +QStringList CleanDialog::checkedFiles() const +{ + QStringList rc; + if (const int rowCount = d->m_filesModel->rowCount()) { + for (int r = 0; r < rowCount; r++) { + const QStandardItem *item = d->m_filesModel->item(r, 0); + if (item->checkState() == Qt::Checked) + rc.push_back(item->data(fileNameRole).toString()); + } + } + return rc; +} + +void CleanDialog::accept() +{ + if (promptToDelete()) + QDialog::accept(); +} + +bool CleanDialog::promptToDelete() +{ + // Prompt the user and delete files + const QStringList selectedFiles = checkedFiles(); + if (selectedFiles.isEmpty()) + return true; + + if (QMessageBox::question(this, tr("Delete"), + tr("Do you want to delete %n files?", 0, selectedFiles.size()), + QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes) + return false; + + // Remove in background + CleanFilesTask *cleanTask = new CleanFilesTask(d->m_workingDirectory, selectedFiles); + connect(cleanTask, SIGNAL(error(QString)), + VCSBase::VCSBaseOutputWindow::instance(), SLOT(appendSilently(QString)), + Qt::QueuedConnection); + + QFuture<void> task = QtConcurrent::run(cleanTask, &CleanFilesTask::run); + const QString taskName = tr("Cleaning %1").arg(d->m_workingDirectory); + Core::ICore::instance()->progressManager()->addTask(task, taskName, + QLatin1String("VCSBase.cleanRepository")); + return true; +} + +void CleanDialog::slotDoubleClicked(const QModelIndex &index) +{ + // Open file on doubleclick + if (const QStandardItem *item = d->m_filesModel->itemFromIndex(index)) + if (!item->data(isDirectoryRole).toBool()) { + const QString fname = item->data(fileNameRole).toString(); + Core::EditorManager::instance()->openEditor(fname); + } +} + +void CleanDialog::changeEvent(QEvent *e) +{ + QDialog::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + d->ui.retranslateUi(this); + break; + default: + break; + } +} + +} // namespace VCSBase + +#include "cleandialog.moc" diff --git a/src/plugins/vcsbase/cleandialog.h b/src/plugins/vcsbase/cleandialog.h new file mode 100644 index 00000000000..0b736466b17 --- /dev/null +++ b/src/plugins/vcsbase/cleandialog.h @@ -0,0 +1,74 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#ifndef CLEANDIALOG_H +#define CLEANDIALOG_H + +#include "vcsbase_global.h" + +#include <QtGui/QDialog> + +QT_BEGIN_NAMESPACE +class QModelIndex; +QT_END_NAMESPACE + +namespace VCSBase { +struct CleanDialogPrivate; + +/* CleanDialog: Completely clean a directory under version control + * from all files that are not under version control based on a list + * generated from the version control system. Presents the user with + * a checkable list of files and/or directories. Double click opens a file. */ + +class VCSBASE_EXPORT CleanDialog : public QDialog { + Q_OBJECT +public: + explicit CleanDialog(QWidget *parent = 0); + virtual ~CleanDialog(); + + void setFileList(const QString &workingDirectory, const QStringList &); + +public slots: + virtual void accept(); + +protected: + void changeEvent(QEvent *e); + +private slots: + void slotDoubleClicked(const QModelIndex &); + +private: + QStringList checkedFiles() const; + bool promptToDelete(); + + CleanDialogPrivate *d; +}; + +} // namespace VCSBase +#endif // CLEANDIALOG_H diff --git a/src/plugins/vcsbase/cleandialog.ui b/src/plugins/vcsbase/cleandialog.ui new file mode 100644 index 00000000000..2888100a3f6 --- /dev/null +++ b/src/plugins/vcsbase/cleandialog.ui @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VCSBase::CleanDialog</class> + <widget class="QDialog" name="VCSBase::CleanDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>682</width> + <height>659</height> + </rect> + </property> + <property name="windowTitle"> + <string>Clean repository</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeView" name="filesTreeView"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>VCSBase::CleanDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>VCSBase::CleanDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/plugins/vcsbase/vcsbase.pro b/src/plugins/vcsbase/vcsbase.pro index 6dec3b25850..497d7d732b4 100644 --- a/src/plugins/vcsbase/vcsbase.pro +++ b/src/plugins/vcsbase/vcsbase.pro @@ -25,7 +25,8 @@ HEADERS += vcsbase_global.h \ checkoutprogresswizardpage.h \ checkoutjobs.h \ basecheckoutwizardpage.h \ - vcsbaseoutputwindow.h + vcsbaseoutputwindow.h \ + cleandialog.h SOURCES += vcsplugin.cpp \ vcsbaseplugin.cpp \ @@ -47,13 +48,15 @@ SOURCES += vcsplugin.cpp \ checkoutprogresswizardpage.cpp \ checkoutjobs.cpp \ basecheckoutwizardpage.cpp \ - vcsbaseoutputwindow.cpp + vcsbaseoutputwindow.cpp \ + cleandialog.cpp RESOURCES += vcsbase.qrc FORMS += vcsbasesettingspage.ui \ nicknamedialog.ui \ checkoutprogresswizardpage.ui \ - basecheckoutwizardpage.ui + basecheckoutwizardpage.ui \ + cleandialog.ui OTHER_FILES += VCSBase.pluginspec -- GitLab