Skip to content
Snippets Groups Projects
branchmodel.cpp 17 KiB
Newer Older
/**************************************************************************
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
** Contact: Nokia Corporation (qt-info@nokia.com)
**
**
** GNU Lesser General Public License Usage
**
hjk's avatar
hjk committed
** 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.
con's avatar
con committed
** In addition, as a special exception, Nokia gives you certain additional
hjk's avatar
hjk committed
** rights. These rights are described in the Nokia Qt LGPL Exception
con's avatar
con committed
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
con's avatar
con committed
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**************************************************************************/

#include "branchmodel.h"
#include "gitclient.h"

#include <utils/qtcassert.h>
Tobias Hunger's avatar
Tobias Hunger committed
#include <vcsbase/vcsbaseoutputwindow.h>

#include <QtGui/QFont>
#include <QtCore/QRegExp>
#include <QtCore/QTimer>

namespace Git {
Tobias Hunger's avatar
Tobias Hunger committed
namespace Internal {

// --------------------------------------------------------------------------
// BranchNode:
// --------------------------------------------------------------------------
Tobias Hunger's avatar
Tobias Hunger committed
class BranchNode
Tobias Hunger's avatar
Tobias Hunger committed
public:
    BranchNode() :
        parent(0), current(false)
    { }

    BranchNode(const QString &n, const QString &s = QString(), bool c = false) :
        parent(0), current(c), name(n), sha(s)
    { }

    ~BranchNode()
    {
        qDeleteAll(children);
    }
Tobias Hunger's avatar
Tobias Hunger committed
    BranchNode *rootNode() const
Tobias Hunger's avatar
Tobias Hunger committed
        return parent ? parent->rootNode() : const_cast<BranchNode *>(this);
Tobias Hunger's avatar
Tobias Hunger committed
    int count() const
Tobias Hunger's avatar
Tobias Hunger committed
    {
        return children.count();
    }

Tobias Hunger's avatar
Tobias Hunger committed
    bool isLeaf() const
Tobias Hunger's avatar
Tobias Hunger committed
    {
        return children.isEmpty();
    }

Tobias Hunger's avatar
Tobias Hunger committed
    bool childOf(BranchNode *node) const
Tobias Hunger's avatar
Tobias Hunger committed
    {
        if (this == node)
            return true;
        return parent ? parent->childOf(node) : false;
    }

Tobias Hunger's avatar
Tobias Hunger committed
    bool isLocal() const
Tobias Hunger's avatar
Tobias Hunger committed
    {
        BranchNode *rn = rootNode();
        if (rn->isLeaf())
            return false;
        return childOf(rn->children.at(0));
    }

Tobias Hunger's avatar
Tobias Hunger committed
    BranchNode *childOfName(const QString &name) const
Tobias Hunger's avatar
Tobias Hunger committed
    {
        for (int i = 0; i < children.count(); ++i) {
            if (children.at(i)->name == name)
                return children.at(i);
        }
        return 0;
    }

Tobias Hunger's avatar
Tobias Hunger committed
    QStringList fullName() const
        QTC_ASSERT(isLeaf(), return QStringList());
Tobias Hunger's avatar
Tobias Hunger committed

        QStringList fn;
Tobias Hunger's avatar
Tobias Hunger committed
        QList<const BranchNode *> nodes;
        const BranchNode *current = this;
Tobias Hunger's avatar
Tobias Hunger committed
        while (current->parent) {
            nodes.prepend(current);
            current = current->parent;
        }

        if (current->children.at(0) == nodes.at(0))
            nodes.removeFirst(); // remove local branch designation

Tobias Hunger's avatar
Tobias Hunger committed
        foreach (const BranchNode *n, nodes)
Tobias Hunger's avatar
Tobias Hunger committed
            fn.append(n->name);

        return fn;
    }

    void insert(const QStringList path, BranchNode *n)
    {
        BranchNode *current = this;
        for (int i = 0; i < path.count(); ++i) {
            BranchNode *c = current->childOfName(path.at(i));
            if (c)
                current = c;
            else
                current = current->append(new BranchNode(path.at(i)));
        }
        current->append(n);
    }

    BranchNode *append(BranchNode *n)
    {
        n->parent = this;
        children.append(n);
        return n;
    }

Tobias Hunger's avatar
Tobias Hunger committed
    QStringList childrenNames() const
Tobias Hunger's avatar
Tobias Hunger committed
    {
        if (children.count() > 0) {
            QStringList names;
            foreach (BranchNode *n, children) {
                names.append(n->childrenNames());
            }
            return names;
        }
        return QStringList(fullName().join(QString('/')));
    }
Tobias Hunger's avatar
Tobias Hunger committed
    BranchNode *parent;
    QList<BranchNode *> children;

    bool current;
    QString name;
    QString sha;
    mutable QString toolTip;
};

// --------------------------------------------------------------------------
// BranchModel:
// --------------------------------------------------------------------------

BranchModel::BranchModel(GitClient *client, QObject *parent) :
    QAbstractItemModel(parent),
    m_client(client),
    m_rootNode(new BranchNode)
    QTC_CHECK(m_client);
Tobias Hunger's avatar
Tobias Hunger committed
    m_rootNode->append(new BranchNode(tr("Local Branches")));
Tobias Hunger's avatar
Tobias Hunger committed
BranchModel::~BranchModel()
Tobias Hunger's avatar
Tobias Hunger committed
    delete m_rootNode;
Tobias Hunger's avatar
Tobias Hunger committed
QModelIndex BranchModel::index(int row, int column, const QModelIndex &parent) const
Tobias Hunger's avatar
Tobias Hunger committed
    BranchNode *node = m_rootNode;
    if (parent.isValid())
        node = static_cast<BranchNode *>(parent.internalPointer());
    if (row >= node->count())
        return QModelIndex();
    return createIndex(row, column, static_cast<void *>(node->children.at(row)));
Tobias Hunger's avatar
Tobias Hunger committed
QModelIndex BranchModel::parent(const QModelIndex &index) const
Tobias Hunger's avatar
Tobias Hunger committed
    BranchNode *node = static_cast<BranchNode *>(index.internalPointer());
    if (node->parent == m_rootNode)
        return QModelIndex();
    int row = node->parent->children.indexOf(node);
    return createIndex(row, 0, static_cast<void *>(node->parent));
Tobias Hunger's avatar
Tobias Hunger committed
int BranchModel::rowCount(const QModelIndex &parent) const
Tobias Hunger's avatar
Tobias Hunger committed
    if (!parent.isValid())
        return m_rootNode->count();
    if (parent.column() != 0)
        return 0;
    return static_cast<BranchNode *>(parent.internalPointer())->count();
Tobias Hunger's avatar
Tobias Hunger committed
int BranchModel::columnCount(const QModelIndex &parent) const
Tobias Hunger's avatar
Tobias Hunger committed
    Q_UNUSED(parent);
    return 1;
Tobias Hunger's avatar
Tobias Hunger committed
QVariant BranchModel::data(const QModelIndex &index, int role) const
Tobias Hunger's avatar
Tobias Hunger committed
    BranchNode *node = static_cast<BranchNode *>(index.internalPointer());

    switch (role) {
Tobias Hunger's avatar
Tobias Hunger committed
    case Qt::DisplayRole:
    case Qt::EditRole:
        return node->name;
    case Qt::ToolTipRole:
        if (!node->isLeaf())
            return QVariant();
        if (node->toolTip.isEmpty())
            node->toolTip = toolTip(node->sha);
        return node->toolTip;
    case Qt::FontRole:
    {
        QFont font;
        if (!node->isLeaf()) {
            font.setBold(true);
        } else if (node->current) {
            font.setBold(true);
            font.setUnderline(true);
        }
        return font;
    }
    default:
        return QVariant();
Tobias Hunger's avatar
Tobias Hunger committed
bool BranchModel::setData(const QModelIndex &index, const QVariant &value, int role)
Tobias Hunger's avatar
Tobias Hunger committed
    if (role != Qt::EditRole)
        return false;
    BranchNode *node = static_cast<BranchNode *>(index.internalPointer());

    const QString newName = value.toString();
    if (newName.isEmpty())
        return false;

    if (node->name == newName)
        return true;

    QStringList oldFullName = node->fullName();
    node->name = newName;
    QStringList newFullName = node->fullName();
    QString output;
    QString errorMessage;
Tobias Hunger's avatar
Tobias Hunger committed
    if (!m_client->synchronousBranchCmd(m_workingDirectory,
                                        QStringList() << QLatin1String("-m")
                                                      << oldFullName.last()
                                                      << newFullName.last(),
                                        &output, &errorMessage)) {
        node->name = oldFullName.last();
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
Tobias Hunger's avatar
Tobias Hunger committed
        return false;
    }

    emit dataChanged(index, index);
    return true;
Tobias Hunger's avatar
Tobias Hunger committed
Qt::ItemFlags BranchModel::flags(const QModelIndex &index) const
Tobias Hunger's avatar
Tobias Hunger committed
    BranchNode *node = static_cast<BranchNode *>(index.internalPointer());
    if (node->isLeaf() && node->isLocal())
        return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled;
    else
        return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
Tobias Hunger's avatar
Tobias Hunger committed
void BranchModel::clear()
Tobias Hunger's avatar
Tobias Hunger committed
    while (m_rootNode->count() > 1) {
        BranchNode *n = m_rootNode->children.takeLast();
        delete n;
    }
    BranchNode *locals = m_rootNode->children.at(0);
    while (locals->count()) {
        BranchNode *n = locals->children.takeLast();
        delete n;
Tobias Hunger's avatar
Tobias Hunger committed
bool BranchModel::refresh(const QString &workingDirectory, QString *errorMessage)
Tobias Hunger's avatar
Tobias Hunger committed
    if (workingDirectory.isEmpty())
        return false;

Tobias Hunger's avatar
Tobias Hunger committed
    branchArgs << QLatin1String(GitClient::noColorOption)
               << QLatin1String("-v") << QLatin1String("-a");
    QString output;
Tobias Hunger's avatar
Tobias Hunger committed
    if (!m_client->synchronousBranchCmd(workingDirectory, branchArgs, &output, errorMessage)) {
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(*errorMessage);
        return false;
    }
Tobias Hunger's avatar
Tobias Hunger committed

    beginResetModel();

    clear();

    m_workingDirectory = workingDirectory;
    const QStringList lines = output.split(QLatin1Char('\n'));
    foreach (const QString &l, lines)
        parseOutputLine(l);

    endResetModel();
Tobias Hunger's avatar
Tobias Hunger committed
void BranchModel::renameBranch(const QString &oldName, const QString &newName)
Tobias Hunger's avatar
Tobias Hunger committed
    QString errorMessage;
    QString output;
    if (!m_client->synchronousBranchCmd(m_workingDirectory,
                                        QStringList() << QLatin1String("-m") << oldName << newName,
                                        &output, &errorMessage))
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
Tobias Hunger's avatar
Tobias Hunger committed
    else
        refresh(m_workingDirectory, &errorMessage);
Tobias Hunger's avatar
Tobias Hunger committed
QString BranchModel::workingDirectory() const
Tobias Hunger's avatar
Tobias Hunger committed
    return m_workingDirectory;
Tobias Hunger's avatar
Tobias Hunger committed
GitClient *BranchModel::client() const
Tobias Hunger's avatar
Tobias Hunger committed
    return m_client;
Tobias Hunger's avatar
Tobias Hunger committed
QModelIndex BranchModel::currentBranch() const
Tobias Hunger's avatar
Tobias Hunger committed
    if (!m_rootNode || !m_rootNode->count())
        return QModelIndex();
    BranchNode *localBranches = m_rootNode->children.at(0);
    QModelIndex localIdx = index(0, 0, QModelIndex());
    for (int i = 0; i < localBranches->count(); ++i) {
        if (localBranches->children.at(i)->current)
            return index(i, 0, localIdx);
    }
    return QModelIndex();
Tobias Hunger's avatar
Tobias Hunger committed
QString BranchModel::branchName(const QModelIndex &idx) const
Tobias Hunger's avatar
Tobias Hunger committed
    if (!idx.isValid())
        return QString();
    BranchNode *node = static_cast<BranchNode *>(idx.internalPointer());
    if (!node->isLeaf())
        return QString();
    QStringList path = node->fullName();
    return path.join(QString('/'));
Tobias Hunger's avatar
Tobias Hunger committed
QStringList BranchModel::localBranchNames() const
Tobias Hunger's avatar
Tobias Hunger committed
    if (!m_rootNode || m_rootNode->children.isEmpty())
        return QStringList();

    return m_rootNode->children.at(0)->childrenNames();
Tobias Hunger's avatar
Tobias Hunger committed
QString BranchModel::sha(const QModelIndex &idx) const
Tobias Hunger's avatar
Tobias Hunger committed
    if (!idx.isValid())
        return QString();
    BranchNode *node = static_cast<BranchNode *>(idx.internalPointer());
    return node->sha;
}
Tobias Hunger's avatar
Tobias Hunger committed
bool BranchModel::isLocal(const QModelIndex &idx) const
{
    if (!idx.isValid())
        return false;
    BranchNode *node = static_cast<BranchNode *>(idx.internalPointer());
    return node->isLocal();
Tobias Hunger's avatar
Tobias Hunger committed
bool BranchModel::isLeaf(const QModelIndex &idx) const
Tobias Hunger's avatar
Tobias Hunger committed
    if (!idx.isValid())
        return false;
    BranchNode *node = static_cast<BranchNode *>(idx.internalPointer());
    return node->isLeaf();
Tobias Hunger's avatar
Tobias Hunger committed
void BranchModel::removeBranch(const QModelIndex &idx)
Tobias Hunger's avatar
Tobias Hunger committed
    QString branch = branchName(idx);
    if (branch.isEmpty())
        return;

    QString errorMessage;
    QString output;
    QStringList args;

    args << QLatin1String("-D") << branch;
    if (!m_client->synchronousBranchCmd(m_workingDirectory, args, &output, &errorMessage)) {
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
Tobias Hunger's avatar
Tobias Hunger committed
        return;
    }

    QModelIndex parentIdx = parent(idx);
    beginRemoveRows(parentIdx, idx.row(), idx.row());
    static_cast<BranchNode *>(parentIdx.internalPointer())->children.removeAt(parentIdx.row());
    delete static_cast<BranchNode *>(idx.internalPointer());
    endRemoveRows();
Tobias Hunger's avatar
Tobias Hunger committed
void BranchModel::checkoutBranch(const QModelIndex &idx)
Tobias Hunger's avatar
Tobias Hunger committed
    QString branch = branchName(idx);
    if (branch.isEmpty())
        return;

    QString errorMessage;
    switch (m_client->ensureStash(m_workingDirectory, &errorMessage)) {
    case GitClient::StashUnchanged:
    case GitClient::Stashed:
    case GitClient::NotStashed:
        break;
    case GitClient::StashCanceled:
        return;
    case GitClient::StashFailed:
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
Tobias Hunger's avatar
Tobias Hunger committed
        return;
    }
    if (m_client->synchronousCheckoutBranch(m_workingDirectory, branch, &errorMessage)) {
        if (errorMessage.isEmpty()) {
            QModelIndex currentIdx = currentBranch();
            if (currentIdx.isValid()) {
                static_cast<BranchNode *>(currentIdx.internalPointer())->current = false;
                emit dataChanged(currentBranch(), currentBranch());
            }
Tobias Hunger's avatar
Tobias Hunger committed
            static_cast<BranchNode *>(idx.internalPointer())->current = true;
            emit dataChanged(idx, idx);
        } else {
            refresh(m_workingDirectory, &errorMessage); // not sure all went well... better refresh!
        }
    }
    if (!errorMessage.isEmpty())
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
Tobias Hunger's avatar
Tobias Hunger committed
bool BranchModel::branchIsMerged(const QModelIndex &idx)
Tobias Hunger's avatar
Tobias Hunger committed
    QString branch = branchName(idx);
    if (branch.isEmpty())
        return false;

    QString errorMessage;
    QString output;
    QStringList args;

    args << QLatin1String("-a") << QLatin1String("--contains") << sha(idx);
Tobias Hunger's avatar
Tobias Hunger committed
    if (!m_client->synchronousBranchCmd(m_workingDirectory, args, &output, &errorMessage)) {
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
    QStringList lines = output.split(QLatin1Char('\n'), QString::SkipEmptyParts);
Tobias Hunger's avatar
Tobias Hunger committed
    foreach (const QString &l, lines) {
        QString currentBranch = l.mid(2); // remove first letters (those are either
                                          // "  " or "* " depending on whether it is
                                          // the currently checked out branch or not)
        if (currentBranch != branch)
Tobias Hunger's avatar
Tobias Hunger committed
            return true;
    }
    return false;
Tobias Hunger's avatar
Tobias Hunger committed
QModelIndex BranchModel::addBranch(const QString &branchName, bool track, const QString &startPoint)
Tobias Hunger's avatar
Tobias Hunger committed
    if (!m_rootNode || !m_rootNode->count())
        return QModelIndex();

    QString output;
    QString errorMessage;

    QStringList args;
    args << (track ? QLatin1String("--track") : QLatin1String("--no-track"));
    args << branchName << startPoint;

    if (!m_client->synchronousBranchCmd(m_workingDirectory, args, &output, &errorMessage)) {
hjk's avatar
hjk committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
Tobias Hunger's avatar
Tobias Hunger committed
        return QModelIndex();
    }

    BranchNode *local = m_rootNode->children.at(0);
    int pos = 0;
    for (pos = 0; pos < local->count(); ++pos) {
        if (local->children.at(pos)->name > branchName)
            break;
    }
    BranchNode *newNode = new BranchNode(branchName);

    // find the sha of the new branch:
    output = toolTip(branchName); // abuse toolTip to get the data;-)
    QStringList lines = output.split(QLatin1Char('\n'));
    foreach (const QString &l, lines) {
        if (l.startsWith("commit ")) {
            newNode->sha = l.mid(7, 8);
            break;
        }
    }

    beginInsertRows(index(0, 0), pos, pos);
    newNode->parent = local;
    local->children.insert(pos, newNode);
    endInsertRows();

    return index(pos, 0, index(0, 0));
Tobias Hunger's avatar
Tobias Hunger committed
void BranchModel::parseOutputLine(const QString &line)
{
    if (line.size() < 3)
        return;

    bool current = line.startsWith(QLatin1String("* "));

    const QString branchInfo = line.mid(2);
    if (current && branchInfo.startsWith(QLatin1String("(no branch)")))
        return;

    QStringList tokens = branchInfo.split(QLatin1Char(' '), QString::SkipEmptyParts);
    if (tokens.size() < 2)
        return;

    QString sha = tokens.at(1);

    // insert node into tree:
    QStringList nameParts = tokens.at(0).split(QLatin1Char('/'));
    if (nameParts.count() < 1)
        return;

    QString name = nameParts.last();
    nameParts.removeLast();

    if (nameParts.isEmpty() || nameParts.at(0) != QLatin1String("remotes")) {
        // local branch:
        while (nameParts.count() > 2)
            nameParts[1] = nameParts.at(1) + QLatin1Char('/') + nameParts.at(2);
        m_rootNode->children.at(0)->insert(nameParts, new BranchNode(name, sha, current));
    } else {
        // remote branch:
        nameParts.removeFirst(); // remove "remotes"
        while (nameParts.count() > 2)
            nameParts[1] = nameParts.at(1) + QLatin1Char('/') + nameParts.at(2);
        m_rootNode->insert(nameParts, new BranchNode(name, sha, current));
    }
Tobias Hunger's avatar
Tobias Hunger committed

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;
Tobias Hunger's avatar
Tobias Hunger committed
} // namespace Internal
} // namespace Git