From defc270896bfc852faa7cad19b154a314b2661e7 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint <qtc-committer@nokia.com> Date: Fri, 19 Dec 2008 17:42:08 +0100 Subject: [PATCH] Fixes: Start a git branch dialog. --- src/plugins/git/branchdialog.cpp | 174 +++++++++++++++++++++++++++++++ src/plugins/git/branchdialog.h | 60 +++++++++++ src/plugins/git/branchdialog.ui | 95 +++++++++++++++++ src/plugins/git/branchmodel.cpp | 136 ++++++++++++++++++++++++ src/plugins/git/branchmodel.h | 58 +++++++++++ src/plugins/git/git.pro | 14 +-- src/plugins/git/gitclient.cpp | 42 ++++++++ src/plugins/git/gitclient.h | 6 ++ src/plugins/git/gitplugin.cpp | 21 +++- 9 files changed, 596 insertions(+), 10 deletions(-) create mode 100644 src/plugins/git/branchdialog.cpp create mode 100644 src/plugins/git/branchdialog.h create mode 100644 src/plugins/git/branchdialog.ui create mode 100644 src/plugins/git/branchmodel.cpp create mode 100644 src/plugins/git/branchmodel.h diff --git a/src/plugins/git/branchdialog.cpp b/src/plugins/git/branchdialog.cpp new file mode 100644 index 00000000000..0918f2d6c07 --- /dev/null +++ b/src/plugins/git/branchdialog.cpp @@ -0,0 +1,174 @@ +#include "branchdialog.h" +#include "branchmodel.h" +#include "gitclient.h" +#include "ui_branchdialog.h" + +#include <QtGui/QItemSelectionModel> +#include <QtGui/QPushButton> +#include <QtGui/QMessageBox> + +// Single selection helper +static inline int selectedRow(const QAbstractItemView *listView) +{ + const QModelIndexList indexList = listView->selectionModel()->selectedIndexes(); + if (indexList.size() == 1) + return indexList.front().row(); + return -1; +} + +namespace Git { + namespace Internal { + +BranchDialog::BranchDialog(QWidget *parent) : + QDialog(parent), + m_client(0), + m_ui(new Ui::BranchDialog), + m_checkoutButton(0), + m_deleteButton(0), + m_localModel(0), + m_remoteModel(0) +{ + m_ui->setupUi(this); + m_checkoutButton = m_ui->buttonBox->addButton(tr("Checkout"), QDialogButtonBox::AcceptRole); + connect(m_checkoutButton, SIGNAL(clicked()), this, SLOT(slotCheckoutSelectedBranch())); + + m_deleteButton = m_ui->buttonBox->addButton(tr("Delete"), QDialogButtonBox::ActionRole); + connect(m_deleteButton, SIGNAL(clicked()), this, SLOT(slotDeleteSelectedBranch())); + + connect(m_ui->localBranchListView, SIGNAL(doubleClicked(QModelIndex)), this, + SLOT(slotLocalBranchActivated())); +} + +BranchDialog::~BranchDialog() +{ + delete m_ui; +} + +bool BranchDialog::init(GitClient *client, const QString &workingDirectory, QString *errorMessage) +{ + // Find repository and populate models. + m_client = client; + m_repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory); + if (m_repoDirectory.isEmpty()) { + *errorMessage = tr("Unable to find the repository directory for '%1'.").arg(workingDirectory); + return false; + } + m_ui->repositoryFieldLabel->setText(m_repoDirectory); + + m_localModel = new BranchModel(client, BranchModel::LocalBranches, this); + m_remoteModel = new BranchModel(client, BranchModel::RemoteBranches, this); + if (!m_localModel->refresh(workingDirectory, errorMessage) + || !m_remoteModel->refresh(workingDirectory, errorMessage)) + return false; + + m_ui->localBranchListView->setModel(m_localModel); + m_ui->remoteBranchListView->setModel(m_remoteModel); + // Selection model comes into existence only now + connect(m_ui->localBranchListView->selectionModel(), + SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(slotEnableButtons())); + connect(m_ui->remoteBranchListView->selectionModel(), + SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(slotEnableButtons())); + slotEnableButtons(); + return true; +} + +int BranchDialog::selectedLocalBranchIndex() const +{ + return selectedRow(m_ui->localBranchListView); +} + +int BranchDialog::selectedRemoteBranchIndex() const +{ + return selectedRow(m_ui->remoteBranchListView); +} + +void BranchDialog::slotEnableButtons() +{ + // We can switch to or delete branches that are not current. + const int selectedLocalRow = selectedLocalBranchIndex(); + const int currentLocalBranch = m_localModel->currentBranch(); + + const bool hasSelection = selectedLocalRow != -1; + const bool currentIsNotSelected = hasSelection && selectedLocalRow != currentLocalBranch; + + m_checkoutButton->setEnabled(currentIsNotSelected); + m_deleteButton->setEnabled(currentIsNotSelected); +} + +bool BranchDialog::ask(const QString &title, const QString &what, bool defaultButton) +{ + return QMessageBox::question(this, title, what, QMessageBox::Yes|QMessageBox::No, + defaultButton ? QMessageBox::Yes : QMessageBox::No) == QMessageBox::Yes; +} + +/* Prompt to delete a local branch and do so. */ +void BranchDialog::slotDeleteSelectedBranch() +{ + const int idx = selectedLocalBranchIndex(); + if (idx == -1) + return; + const QString name = m_localModel->branchName(idx); + if (!ask(tr("Delete Branch"), tr("Would you like to delete the branch '%1'?").arg(name), true)) + return; + QString errorMessage; + bool ok = false; + do { + QString output; + QStringList args(QLatin1String("-D")); + args << name; + if (!m_client->synchronousBranchCmd(m_repoDirectory, args, &output, &errorMessage)) + break; + if (!m_localModel->refresh(m_repoDirectory, &errorMessage)) + break; + ok = true; + } while (false); + slotEnableButtons(); + if (!ok) + QMessageBox::warning(this, tr("Failed to delete branch"), errorMessage); +} + +void BranchDialog::slotLocalBranchActivated() +{ + if (m_checkoutButton->isEnabled()) + m_checkoutButton->animateClick(); +} + +/* Ask to stash away changes and then close dialog and do an asynchronous + * checkout. */ +void BranchDialog::slotCheckoutSelectedBranch() +{ + const int idx = selectedLocalBranchIndex(); + if (idx == -1) + return; + const QString name = m_localModel->branchName(idx); + QString errorMessage; + switch (m_client->ensureStash(m_repoDirectory, &errorMessage)) { + case GitClient::StashUnchanged: + case GitClient::Stashed: + case GitClient::NotStashed: + break; + case GitClient::StashCanceled: + return; + case GitClient::StashFailed: + QMessageBox::warning(this, tr("Failed to stash"), errorMessage); + return; + } + accept(); + m_client->checkoutBranch(m_repoDirectory, name); +} + +void BranchDialog::changeEvent(QEvent *e) +{ + switch (e->type()) { + case QEvent::LanguageChange: + m_ui->retranslateUi(this); + break; + default: + break; + } +} + +} // namespace Internal +} // namespace Git diff --git a/src/plugins/git/branchdialog.h b/src/plugins/git/branchdialog.h new file mode 100644 index 00000000000..064719b8ff0 --- /dev/null +++ b/src/plugins/git/branchdialog.h @@ -0,0 +1,60 @@ +#ifndef BRANCHDIALOG_H +#define BRANCHDIALOG_H + +#include <QtGui/QDialog> + +QT_BEGIN_NAMESPACE +class QPushButton; +QT_END_NAMESPACE + +namespace Git { + namespace Internal { + namespace Ui { + class BranchDialog; + } + + class GitClient; + class BranchModel; + + /* Branch dialog: Display a list of local branches at the top + * and remote branches below. Offers to checkout/delete local + * branches. + * TODO: Add new branch (optionally tracking a remote one). + * How to find out that a local branch is a tracking one? */ + class BranchDialog : public QDialog { + Q_OBJECT + Q_DISABLE_COPY(BranchDialog) + public: + explicit BranchDialog(QWidget *parent = 0); + + bool init(GitClient *client, const QString &workingDirectory, QString *errorMessage); + + virtual ~BranchDialog(); + + protected: + virtual void changeEvent(QEvent *e); + + private slots: + void slotEnableButtons(); + void slotCheckoutSelectedBranch(); + void slotDeleteSelectedBranch(); + void slotLocalBranchActivated(); + + private: + bool ask(const QString &title, const QString &what, bool defaultButton); + + int selectedLocalBranchIndex() const; + int selectedRemoteBranchIndex() const; + + GitClient *m_client; + Ui::BranchDialog *m_ui; + QPushButton *m_checkoutButton; + QPushButton *m_deleteButton; + + BranchModel *m_localModel; + BranchModel *m_remoteModel; + QString m_repoDirectory; + }; + } // namespace Internal +} // namespace Git +#endif // BRANCHDIALOG_H diff --git a/src/plugins/git/branchdialog.ui b/src/plugins/git/branchdialog.ui new file mode 100644 index 00000000000..07f7ff656b1 --- /dev/null +++ b/src/plugins/git/branchdialog.ui @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Git::Internal::BranchDialog</class> + <widget class="QDialog" name="Git::Internal::BranchDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>544</width> + <height>631</height> + </rect> + </property> + <property name="windowTitle"> + <string>Branches</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QGroupBox" name="infoGroupBox"> + <property name="title"> + <string>General information</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="repositoryLabel"> + <property name="text"> + <string>Repository:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="repositoryFieldLabel"> + <property name="text"> + <string>TextLabel</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="localBranchGroupBox"> + <property name="title"> + <string>Branches</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListView" name="localBranchListView"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="remoteBranchGroupBox"> + <property name="title"> + <string>Remote branches</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QListView" name="remoteBranchListView"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>Git::Internal::BranchDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>466</x> + <y>614</y> + </hint> + <hint type="destinationlabel"> + <x>544</x> + <y>23</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/plugins/git/branchmodel.cpp b/src/plugins/git/branchmodel.cpp new file mode 100644 index 00000000000..0881aea654b --- /dev/null +++ b/src/plugins/git/branchmodel.cpp @@ -0,0 +1,136 @@ +#include "branchmodel.h" +#include "gitclient.h" + +#include <QtCore/QDebug> + +enum { debug = 0 }; + +namespace Git { + namespace Internal { + +// Parse a branch line: " *name sha description". Return true if it is +// the current one +bool BranchModel::Branch::parse(const QString &lineIn, bool *isCurrent) +{ + if (debug) + qDebug() << Q_FUNC_INFO << lineIn; + + *isCurrent = lineIn.startsWith(QLatin1String("* ")); + if (lineIn.size() < 3) + return false; + + const QStringList tokens =lineIn.mid(2).split(QLatin1Char(' '), QString::SkipEmptyParts); + if (tokens.size() < 2) + return false; + name = tokens.at(0); + currentSHA= tokens.at(1); + toolTip.clear(); + return true; +} + +static inline Qt::ItemFlags typeToModelFlags(BranchModel::Type t) +{ + Qt::ItemFlags rc = Qt::ItemIsSelectable|Qt::ItemIsEnabled; + if (t == BranchModel::BranchModel::LocalBranches) + rc |= Qt::ItemIsUserCheckable; + return rc; +} + +// --- BranchModel +BranchModel::BranchModel(GitClient *client, Type type, QObject *parent) : + QAbstractListModel(parent), + m_type(type), + m_flags(typeToModelFlags(type)), + m_client(client), + m_currentBranch(-1) +{ +} + +int BranchModel::currentBranch() const +{ + return m_currentBranch; +} + +QString BranchModel::branchName(int row) const +{ + return m_branches.at(row).name; +} + +int BranchModel::rowCount(const QModelIndex & /* parent */) const +{ + return m_branches.size(); +} + +QVariant BranchModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + switch (role) { + case Qt::DisplayRole: + return branchName(row); + case Qt::ToolTipRole: + if (m_branches.at(row).toolTip.isEmpty()) + m_branches.at(row).toolTip = toolTip(m_branches.at(row).currentSHA); + return m_branches.at(row).toolTip; + break; + case Qt::CheckStateRole: + if (m_type == RemoteBranches) + return QVariant(); + return row == m_currentBranch ? Qt::Checked : Qt::Unchecked; + default: + break; + } + return QVariant(); +} + +Qt::ItemFlags BranchModel::flags(const QModelIndex & /*index */) const +{ + return m_flags; +} + +bool BranchModel::refresh(const QString &workingDirectory, QString *errorMessage) +{ + // Run branch command with verbose. + QStringList branchArgs(QLatin1String("-v")); + QString output; + if (m_type == RemoteBranches) + branchArgs.push_back(QLatin1String("-r")); + if (!m_client->synchronousBranchCmd(workingDirectory, branchArgs, &output, errorMessage)) + return false; + if (debug) + qDebug() << Q_FUNC_INFO << workingDirectory << output; + // Parse output + m_workingDirectory = workingDirectory; + m_branches.clear(); + m_currentBranch = -1; + const QStringList branches = output.split(QLatin1Char('\n')); + const int branchCount = branches.size(); + bool isCurrent; + for (int b = 0; b < branchCount; b++) { + Branch newBranch; + if (newBranch.parse(branches.at(b), &isCurrent)) { + m_branches.push_back(newBranch); + if (isCurrent) + m_currentBranch = b; + } + } + reset(); + return true; +} + +QString BranchModel::toolTip(const QString &sha) const +{ + // Show the sha description excluding diff as toolTip + QString output; + QString errorMessage; + if (!m_client->synchronousShow(m_workingDirectory, sha, &output, &errorMessage)) + return errorMessage; + // Remove 'diff' output + const int diffPos = output.indexOf(QLatin1String("\ndiff --")); + if (diffPos != -1) + output.remove(diffPos, output.size() - diffPos); + return output; +} + +} +} + diff --git a/src/plugins/git/branchmodel.h b/src/plugins/git/branchmodel.h new file mode 100644 index 00000000000..5083a1c377c --- /dev/null +++ b/src/plugins/git/branchmodel.h @@ -0,0 +1,58 @@ +#ifndef BRANCHMODEL_H +#define BRANCHMODEL_H + +#include <QtCore/QAbstractListModel> +#include <QtCore/QList> + +namespace Git { + namespace Internal { + +class GitClient; + +/* A model to list git branches in a simple list of branch names. Local + * branches will have a read-only checkbox indicating the current one. The + * [delayed] tooltip describes the latest commit. */ + +class BranchModel : public QAbstractListModel { +public: + enum Type { LocalBranches, RemoteBranches }; + + explicit BranchModel(GitClient *client, + Type type = LocalBranches, + QObject *parent = 0); + + bool refresh(const QString &workingDirectory, QString *errorMessage); + + int currentBranch() const; + QString branchName(int row) const; + + // QAbstractListModel + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + +private: + QString toolTip(const QString &sha) const; + + struct Branch { + bool parse(const QString &line, bool *isCurrent); + + QString name; + QString currentSHA; + mutable QString toolTip; + }; + typedef QList<Branch> BranchList; + + const Type m_type; + const Qt::ItemFlags m_flags; + GitClient *m_client; + + QString m_workingDirectory; + BranchList m_branches; + int m_currentBranch; +}; + +} +} + +#endif // BRANCHMODEL_H diff --git a/src/plugins/git/git.pro b/src/plugins/git/git.pro index 3c4ca176cb5..7f4ed2fc0cc 100644 --- a/src/plugins/git/git.pro +++ b/src/plugins/git/git.pro @@ -6,7 +6,6 @@ include(../../plugins/texteditor/texteditor.pri) include(../../plugins/coreplugin/coreplugin.pri) include(../../plugins/vcsbase/vcsbase.pri) include(../../libs/utils/utils.pri) - HEADERS += gitplugin.h \ gitconstants.h \ gitoutputwindow.h \ @@ -19,8 +18,9 @@ HEADERS += gitplugin.h \ gitsubmiteditorwidget.h \ gitsubmiteditor.h \ gitversioncontrol.h \ - gitsettings.h - + gitsettings.h \ + branchdialog.h \ + branchmodel.h SOURCES += gitplugin.cpp \ gitoutputwindow.cpp \ gitclient.cpp \ @@ -32,8 +32,10 @@ SOURCES += gitplugin.cpp \ gitsubmiteditorwidget.cpp \ gitsubmiteditor.cpp \ gitversioncontrol.cpp \ - gitsettings.cpp - + gitsettings.cpp \ + branchdialog.cpp \ + branchmodel.cpp FORMS += changeselectiondialog.ui \ settingspage.ui \ - gitsubmitpanel.ui + gitsubmitpanel.ui \ + branchdialog.ui diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index 83dafdc1b66..f3d507171d2 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -276,6 +276,13 @@ void GitClient::blame(const QString &workingDirectory, const QString &fileName) executeGit(workingDirectory, arguments, editor); } +void GitClient::checkoutBranch(const QString &workingDirectory, const QString &branch) +{ + QStringList arguments(QLatin1String("checkout")); + arguments << branch; + executeGit(workingDirectory, arguments, 0, true); +} + void GitClient::checkout(const QString &workingDirectory, const QString &fileName) { // Passing an empty argument as the file name is very dangereous, since this makes @@ -395,6 +402,41 @@ bool GitClient::synchronousStash(const QString &workingDirectory, QString *error return true; } +bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs, + QString *output, QString *errorMessage) +{ + if (Git::Constants::debug) + qDebug() << Q_FUNC_INFO << workingDirectory << branchArgs; + branchArgs.push_front(QLatin1String("branch")); + QByteArray outputText; + QByteArray errorText; + const bool rc = synchronousGit(workingDirectory, branchArgs, &outputText, &errorText); + if (!rc) { + *errorMessage = tr("Unable to run branch command: %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText)); + return false; + } + *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r')); + return true; +} + +bool GitClient::synchronousShow(const QString &workingDirectory, const QString &id, + QString *output, QString *errorMessage) +{ + if (Git::Constants::debug) + qDebug() << Q_FUNC_INFO << workingDirectory << id; + QStringList args(QLatin1String("show")); + args << id; + QByteArray outputText; + QByteArray errorText; + const bool rc = synchronousGit(workingDirectory, args, &outputText, &errorText); + if (!rc) { + *errorMessage = tr("Unable to run show: %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText)); + return false; + } + *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r')); + return true; +} + void GitClient::executeGit(const QString &workingDirectory, const QStringList &arguments, VCSBase::VCSBaseEditor* editor, diff --git a/src/plugins/git/gitclient.h b/src/plugins/git/gitclient.h index af45bbb9a36..f09daa00ab8 100644 --- a/src/plugins/git/gitclient.h +++ b/src/plugins/git/gitclient.h @@ -86,6 +86,7 @@ public: void blame(const QString &workingDirectory, const QString &fileName); void showCommit(const QString &workingDirectory, const QString &commit); void checkout(const QString &workingDirectory, const QString &file); + void checkoutBranch(const QString &workingDirectory, const QString &branch); void hardReset(const QString &workingDirectory, const QString &commit); void addFile(const QString &workingDirectory, const QString &fileName); bool synchronousAdd(const QString &workingDirectory, const QStringList &files); @@ -93,6 +94,11 @@ public: bool synchronousReset(const QString &workingDirectory, const QStringList &files, QString *errorMessage); bool synchronousCheckout(const QString &workingDirectory, const QStringList &files, QString *errorMessage); bool synchronousStash(const QString &workingDirectory, QString *errorMessage); + bool synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs, + QString *output, QString *errorMessage); + bool synchronousShow(const QString &workingDirectory, const QString &id, + QString *output, QString *errorMessage); + void pull(const QString &workingDirectory); void push(const QString &workingDirectory); diff --git a/src/plugins/git/gitplugin.cpp b/src/plugins/git/gitplugin.cpp index 85af681d209..fe2d93adbff 100644 --- a/src/plugins/git/gitplugin.cpp +++ b/src/plugins/git/gitplugin.cpp @@ -40,6 +40,7 @@ #include "giteditor.h" #include "gitsubmiteditor.h" #include "gitversioncontrol.h" +#include "branchdialog.h" #include <coreplugin/icore.h> #include <coreplugin/coreconstants.h> @@ -401,7 +402,7 @@ bool GitPlugin::initialize(const QStringList &arguments, QString *error_message) gitContainer->addAction(createSeparator(actionManager, globalcontext, QLatin1String("Git.Sep.Branch"), this)); - m_branchListAction = new QAction(tr("List branches"), this); + m_branchListAction = new QAction(tr("Branches..."), this); command = actionManager->registerAction(m_branchListAction, "Git.BranchList", globalcontext); command->setAttribute(Core::ICommand::CA_UpdateText); connect(m_branchListAction, SIGNAL(triggered()), this, SLOT(branchList())); @@ -494,7 +495,6 @@ QString GitPlugin::getWorkingDirectory() qDebug() << Q_FUNC_INFO << "file" << workingDirectory; if (workingDirectory.isEmpty()) { - m_outputWindow->clearContents(); m_outputWindow->append(tr("Could not find working directory")); m_outputWindow->popup(false); return QString(); @@ -740,8 +740,21 @@ void GitPlugin::stashPop() void GitPlugin::branchList() { const QString workingDirectory = getWorkingDirectory(); - if (!workingDirectory.isEmpty()) - m_gitClient->branchList(workingDirectory); + if (workingDirectory.isEmpty()) + return; +#ifndef USE_BRANCHDIALOG + QString errorMessage; + BranchDialog dialog(m_core->mainWindow()); + + if (!dialog.init(m_gitClient, workingDirectory, &errorMessage)) { + m_outputWindow->append(errorMessage); + m_outputWindow->popup(false); + return; + } + dialog.exec(); +#else + m_gitClient->branchList(workingDirectory); +#endif } void GitPlugin::stashList() -- GitLab