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