diff --git a/src/plugins/git/git.pro b/src/plugins/git/git.pro index fb887a2e55e559d31c13d34b6de66c6d6dda9f0a..873ad6bacd8274daac73b7f4c68a1f59470488a7 100644 --- a/src/plugins/git/git.pro +++ b/src/plugins/git/git.pro @@ -28,7 +28,8 @@ HEADERS += gitplugin.h \ remotemodel.h \ remotedialog.h \ branchadddialog.h \ - resetdialog.h + resetdialog.h \ + mergetool.h SOURCES += gitplugin.cpp \ gitclient.cpp \ @@ -50,7 +51,8 @@ SOURCES += gitplugin.cpp \ remotemodel.cpp \ remotedialog.cpp \ branchadddialog.cpp \ - resetdialog.cpp + resetdialog.cpp \ + mergetool.cpp FORMS += changeselectiondialog.ui \ settingspage.ui \ diff --git a/src/plugins/git/git.qbs b/src/plugins/git/git.qbs index e11a830c964ced94df126d5dbb30a0d50cbd151c..ec5a0af2927b8e37f7956f55a7bc3ebba5e492c7 100644 --- a/src/plugins/git/git.qbs +++ b/src/plugins/git/git.qbs @@ -54,6 +54,8 @@ QtcPlugin { "gitutils.h", "gitversioncontrol.cpp", "gitversioncontrol.h", + "mergetool.cpp", + "mergetool.h", "remoteadditiondialog.ui", "remotedialog.cpp", "remotedialog.h", diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index 4ccdb0132664cbefb0827ce1d9131f5595ff4d5a..61747141ed12215001bf0322a9046985da0b506b 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -35,6 +35,7 @@ #include "gitplugin.h" #include "gitsubmiteditor.h" #include "gitversioncontrol.h" +#include "mergetool.h" #include <vcsbase/submitfilemodel.h> @@ -572,6 +573,13 @@ void GitClient::diffBranch(const QString &workingDirectory, executeGit(workingDirectory, cmdArgs, editor); } +void GitClient::merge(const QString &workingDirectory, const QStringList &unmergedFileNames) +{ + MergeTool *mergeTool = new MergeTool(this); + if (!mergeTool->start(workingDirectory, unmergedFileNames)) + delete mergeTool; +} + void GitClient::status(const QString &workingDirectory) { // @TODO: Use "--no-color" once it is supported diff --git a/src/plugins/git/gitclient.h b/src/plugins/git/gitclient.h index 33ed046617959f25b943f8da703ae10e6dae0151..3a684e606e2b7f4bf30ab4c5e4d5b2f1d2e5a354 100644 --- a/src/plugins/git/gitclient.h +++ b/src/plugins/git/gitclient.h @@ -92,6 +92,7 @@ public: void diffBranch(const QString &workingDirectory, const QStringList &diffArgs, const QString &branchName); + void merge(const QString &workingDirectory, const QStringList &unmergedFileNames = QStringList()); void status(const QString &workingDirectory); void graphLog(const QString &workingDirectory) { graphLog(workingDirectory, QString()); } diff --git a/src/plugins/git/gitplugin.cpp b/src/plugins/git/gitplugin.cpp index ac3cd283c5cf919781902fad648802b115c2c71c..6a0d7d4638dfabf120ba57c54462e15dc1b7d777 100644 --- a/src/plugins/git/gitplugin.cpp +++ b/src/plugins/git/gitplugin.cpp @@ -43,6 +43,7 @@ #include "stashdialog.h" #include "settingspage.h" #include "resetdialog.h" +#include "mergetool.h" #include "gerrit/gerritplugin.h" @@ -519,6 +520,10 @@ bool GitPlugin::initialize(const QStringList &arguments, QString *errorMessage) tr("Amend Last Commit..."), Core::Id("Git.AmendCommit"), globalcontext, true, SLOT(startAmendCommit())); + createRepositoryAction(gitContainer, + tr("Merge Tool"), Core::Id("Git.MergeTool"), + globalcontext, true, SLOT(startMergeTool())); + // Subversion in a submenu. gitContainer->addSeparator(globalcontext); @@ -565,6 +570,11 @@ void GitPlugin::submitEditorDiff(const QStringList &unstaged, const QStringList m_gitClient->diff(m_submitRepository, QStringList(), unstaged, staged); } +void GitPlugin::submitEditorMerge(const QStringList &unmerged) +{ + m_gitClient->merge(m_submitRepository, unmerged); +} + void GitPlugin::diffCurrentFile() { const VcsBase::VcsBasePluginState state = currentState(); @@ -708,6 +718,7 @@ Core::IEditor *GitPlugin::openSubmitEditor(const QString &fileName, const Commit if (amend) // Allow for just correcting the message submitEditor->setEmptyFileListEnabled(true); connect(submitEditor, SIGNAL(diff(QStringList,QStringList)), this, SLOT(submitEditorDiff(QStringList,QStringList))); + connect(submitEditor, SIGNAL(merge(QStringList)), this, SLOT(submitEditorMerge(QStringList))); return editor; } @@ -804,6 +815,13 @@ void GitPlugin::push() m_gitClient->synchronousPush(state.topLevel()); } +void GitPlugin::startMergeTool() +{ + const VcsBase::VcsBasePluginState state = currentState(); + QTC_ASSERT(state.hasTopLevel(), return); + m_gitClient->merge(state.topLevel()); +} + // Retrieve member function of git client stored as user data of action static inline GitClientMemberFunc memberFunctionFromAction(const QObject *o) { diff --git a/src/plugins/git/gitplugin.h b/src/plugins/git/gitplugin.h index 2fdcfd2ee70eaedd8fb83852a58c99dfeaca66e3..c3709fa94b0990f8c05cf95ef97d8f0ff3ba35ea 100644 --- a/src/plugins/git/gitplugin.h +++ b/src/plugins/git/gitplugin.h @@ -105,6 +105,7 @@ private slots: void diffCurrentProject(); void diffRepository(); void submitEditorDiff(const QStringList &unstaged, const QStringList &staged); + void submitEditorMerge(const QStringList &unmerged); void submitCurrentLog(); void logFile(); void blameFile(); @@ -131,6 +132,7 @@ private slots: void fetch(); void pull(); void push(); + void startMergeTool(); #ifdef WITH_TESTS void testStatusParsing_data(); diff --git a/src/plugins/git/gitsubmiteditor.cpp b/src/plugins/git/gitsubmiteditor.cpp index 895efe61a1085e70dd2ca586a843587abb75ab95..c69a3425afcbbcd229daafbd209520d501100fcd 100644 --- a/src/plugins/git/gitsubmiteditor.cpp +++ b/src/plugins/git/gitsubmiteditor.cpp @@ -81,7 +81,8 @@ void GitSubmitEditor::setCommitData(const CommitData &d) void GitSubmitEditor::slotDiffSelected(const QStringList &files) { - // Sort it apart into staged/unstaged files + // Sort it apart into unmerged/staged/unstaged files + QStringList unmergedFiles; QStringList unstagedFiles; QStringList stagedFiles; const int fileColumn = fileNameColumn(); @@ -90,7 +91,9 @@ void GitSubmitEditor::slotDiffSelected(const QStringList &files) const QString fileName = m_model->item(r, fileColumn)->text(); if (files.contains(fileName)) { const FileStates state = static_cast<FileStates>(m_model->extraData(r).toInt()); - if (state & StagedFile) + if (state & UnmergedFile) + unmergedFiles.push_back(fileName); + else if (state & StagedFile) stagedFiles.push_back(fileName); else if (state != UntrackedFile) unstagedFiles.push_back(fileName); @@ -98,6 +101,8 @@ void GitSubmitEditor::slotDiffSelected(const QStringList &files) } if (!unstagedFiles.empty() || !stagedFiles.empty()) emit diff(unstagedFiles, stagedFiles); + if (!unmergedFiles.empty()) + emit merge(unmergedFiles); } GitSubmitEditorPanelData GitSubmitEditor::panelData() const diff --git a/src/plugins/git/gitsubmiteditor.h b/src/plugins/git/gitsubmiteditor.h index 41454ac37ebe82156284c4a613f0381cc7ff5666..2cacaecc2e9fcf837f6c2fb8abb5c1903429d7e4 100644 --- a/src/plugins/git/gitsubmiteditor.h +++ b/src/plugins/git/gitsubmiteditor.h @@ -56,6 +56,7 @@ public: signals: void diff(const QStringList &unstagedFiles, const QStringList &stagedFiles); + void merge(const QStringList &unmergedFiles); protected: QByteArray fileContents() const; diff --git a/src/plugins/git/mergetool.cpp b/src/plugins/git/mergetool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..02845756fa0b440e8d7f60eb0f0ecdb1dbef73e6 --- /dev/null +++ b/src/plugins/git/mergetool.cpp @@ -0,0 +1,232 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** 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. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "gitclient.h" +#include "gitplugin.h" +#include "mergetool.h" + +#include <QMessageBox> +#include <QProcess> +#include <QPushButton> +#include <QRegExp> + +namespace Git { +namespace Internal { + +MergeTool::MergeTool(QObject *parent) : + QObject(parent), + m_process(0) +{ +} + +MergeTool::~MergeTool() +{ + delete m_process; +} + +bool MergeTool::start(const QString &workingDirectory, const QStringList &files) +{ + QStringList arguments; + arguments << QLatin1String("mergetool") << QLatin1String("-y"); + GitClient *client = GitPlugin::instance()->gitClient(); + if (!files.isEmpty()) { + if (client->gitVersion() < 0x010708) { + QMessageBox::warning(0, tr("Error"), tr("Files input for mergetool requires git >= 1.7.8")); + return false; + } + arguments << files; + } + m_process = new QProcess(this); + m_process->setWorkingDirectory(workingDirectory); + m_process->start(QLatin1String("git"), arguments); + if (m_process->waitForStarted()) { + connect(m_process, SIGNAL(finished(int)), this, SLOT(done())); + connect(m_process, SIGNAL(readyRead()), this, SLOT(readData())); + } + else { + delete m_process; + m_process = 0; + return false; + } + return true; +} + +MergeTool::FileState MergeTool::waitAndReadStatus(QString &extraInfo) +{ + QByteArray state; + if (m_process->canReadLine() || (m_process->waitForReadyRead(500) && m_process->canReadLine())) { + state = m_process->readLine().trimmed(); + // " {local}: modified file" + // " {remote}: deleted" + if (!state.isEmpty()) { + state = state.mid(state.indexOf(':') + 2); + if (state == "deleted") + return DeletedState; + if (state.startsWith("modified")) + return ModifiedState; + if (state.startsWith("created")) + return CreatedState; + QByteArray submodulePrefix("submodule commit "); + // " {local}: submodule commit <hash>" + if (state.startsWith(submodulePrefix)) { + extraInfo = QString::fromLocal8Bit(state.mid(submodulePrefix.size())); + return SubmoduleState; + } + // " {local}: a symbolic link -> 'foo.cpp'" + QByteArray symlinkPrefix("a symbolic link -> '"); + if (state.startsWith(symlinkPrefix)) { + extraInfo = QString::fromLocal8Bit(state.mid(symlinkPrefix.size())); + extraInfo.chop(1); // remove last quote + return SymbolicLinkState; + } + } + } + return UnknownState; +} + +static MergeTool::MergeType mergeType(const QByteArray &type) +{ + if (type == "Normal") + return MergeTool::NormalMerge; + if (type == "Deleted") + return MergeTool::DeletedMerge; + if (type == "Submodule") + return MergeTool::SubmoduleMerge; + else + return MergeTool::SymbolicLinkMerge; +} + +QString MergeTool::mergeTypeName() +{ + switch (m_mergeType) { + case NormalMerge: return tr("Normal"); + case SubmoduleMerge: return tr("Submodule"); + case DeletedMerge: return tr("Deleted"); + case SymbolicLinkMerge: return tr("Symbolic link"); + } + return QString(); +} + +QString MergeTool::stateName(MergeTool::FileState state, const QString &extraInfo) +{ + switch (state) { + case ModifiedState: return tr("Modified"); + case CreatedState: return tr("Created"); + case DeletedState: return tr("Deleted"); + case SubmoduleState: return tr("Submodule commit %1").arg(extraInfo); + case SymbolicLinkState: return tr("Symbolic link -> %1").arg(extraInfo); + default: break; + } + return QString(); +} + +void MergeTool::chooseAction() +{ + m_merging = (m_mergeType == NormalMerge); + if (m_merging) + return; + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Merge Conflict")); + msgBox.setIcon(QMessageBox::Question); + msgBox.setStandardButtons(QMessageBox::Abort); + msgBox.setText(tr("%1 merge conflict for '%2'\nLocal: %3\nRemote: %4") + .arg(mergeTypeName()) + .arg(m_fileName) + .arg(stateName(m_localState, m_localInfo)) + .arg(stateName(m_remoteState, m_remoteInfo)) + ); + switch (m_mergeType) { + case SubmoduleMerge: + case SymbolicLinkMerge: + addButton(&msgBox, tr("&Local"), 'l'); + addButton(&msgBox, tr("&Remote"), 'r'); + break; + case DeletedMerge: + if (m_localState == CreatedState || m_remoteState == CreatedState) + addButton(&msgBox, tr("&Created"), 'c'); + else + addButton(&msgBox, tr("&Modified"), 'm'); + addButton(&msgBox, tr("&Deleted"), 'd'); + break; + default: + break; + } + + msgBox.exec(); + QByteArray ba; + QVariant key; + QAbstractButton *button = msgBox.clickedButton(); + if (button) + key = button->property("key"); + // either the message box was closed without clicking anything, or abort was clicked + if (!key.isValid()) + key = QVariant(QLatin1Char('a')); // abort + ba.append(key.toChar().toLatin1()); + ba.append('\n'); + m_process->write(ba); +} + +void MergeTool::addButton(QMessageBox *msgBox, const QString &text, char key) +{ + msgBox->addButton(text, QMessageBox::AcceptRole)->setProperty("key", key); +} + +void MergeTool::readData() +{ + while (m_process->bytesAvailable()) { + QByteArray line = m_process->canReadLine() ? m_process->readLine() : m_process->readAllStandardOutput(); + // {Normal|Deleted|Submodule|Symbolic link} merge conflict for 'foo.cpp' + int index = line.indexOf(" merge conflict for "); + if (index != -1) { + m_mergeType = mergeType(line.left(index)); + int quote = line.indexOf('\''); + m_fileName = QString::fromLocal8Bit(line.mid(quote + 1, line.lastIndexOf('\'') - quote - 1)); + m_localState = waitAndReadStatus(m_localInfo); + m_remoteState = waitAndReadStatus(m_remoteInfo); + chooseAction(); + } else if (m_merging && line.startsWith("Continue merging")) { + if (QMessageBox::question(0, tr("Continue Merging"), + tr("Continue merging other unresolved paths?"), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { + m_process->write("y\n"); + } else { + m_process->write("n\n"); + } + } + } +} + +void MergeTool::done() +{ + QMessageBox::information(0, tr("Done"), tr("Merge done")); + deleteLater(); +} + +} // namespace Internal +} // namespace Git diff --git a/src/plugins/git/mergetool.h b/src/plugins/git/mergetool.h new file mode 100644 index 0000000000000000000000000000000000000000..03cc5854b1836a14cdb6c673d4552c6fffb94e72 --- /dev/null +++ b/src/plugins/git/mergetool.h @@ -0,0 +1,93 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** 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. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#ifndef MERGETOOL_H +#define MERGETOOL_H + +#include <QObject> +#include <QStringList> + +QT_BEGIN_NAMESPACE +class QProcess; +class QMessageBox; +QT_END_NAMESPACE + +namespace Git { +namespace Internal { + +class MergeTool : public QObject +{ + Q_OBJECT + + enum FileState { + UnknownState, + ModifiedState, + CreatedState, + DeletedState, + SubmoduleState, + SymbolicLinkState + }; + +public: + explicit MergeTool(QObject *parent = 0); + ~MergeTool(); + bool start(const QString &workingDirectory, const QStringList &files = QStringList()); + + enum MergeType { + NormalMerge, + SubmoduleMerge, + DeletedMerge, + SymbolicLinkMerge + }; + +private slots: + void readData(); + void done(); + +private: + FileState waitAndReadStatus(QString &extraInfo); + QString mergeTypeName(); + QString stateName(FileState state, const QString &extraInfo); + void chooseAction(); + void addButton(QMessageBox *msgBox, const QString &text, char key); + + QProcess *m_process; + MergeType m_mergeType; + QString m_fileName; + FileState m_localState; + QString m_localInfo; + FileState m_remoteState; + QString m_remoteInfo; + bool m_merging; +}; + +} // namespace Internal +} // namespace Git + +#endif // MERGETOOL_H