// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "mergetool.h" #include "gitclient.h" #include "gitplugin.h" #include "gittr.h" #include #include #include #include #include #include #include using namespace Utils; using namespace VcsBase; namespace Git::Internal { MergeTool::MergeTool(QObject *parent) : QObject(parent) { connect(&m_process, &Process::done, this, &MergeTool::done); connect(&m_process, &Process::readyReadStandardOutput, this, &MergeTool::readData); Environment env = Environment::systemEnvironment(); env.set("LANG", "C"); env.set("LANGUAGE", "C"); m_process.setEnvironment(env); m_process.setProcessMode(ProcessMode::Writer); m_process.setProcessChannelMode(QProcess::MergedChannels); } void MergeTool::start(const FilePath &workingDirectory, const QStringList &files) { const CommandLine cmd{gitClient().vcsBinary(workingDirectory), {"mergetool", "-y", files}}; VcsOutputWindow::appendCommand(workingDirectory, cmd); m_process.setCommand(cmd); m_process.setWorkingDirectory(workingDirectory); m_process.start(); } MergeTool::FileState MergeTool::parseStatus(const QString &line, QString &extraInfo) { QString state = line.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; const QString submodulePrefix("submodule commit "); // " {local}: submodule commit " if (state.startsWith(submodulePrefix)) { extraInfo = state.mid(submodulePrefix.size()); return SubmoduleState; } // " {local}: a symbolic link -> 'foo.cpp'" const QString symlinkPrefix("a symbolic link -> '"); if (state.startsWith(symlinkPrefix)) { extraInfo = state.mid(symlinkPrefix.size()); extraInfo.chop(1); // remove last quote return SymbolicLinkState; } } return UnknownState; } static MergeTool::MergeType mergeType(const QString &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::tr("Normal"); case SubmoduleMerge: return Tr::tr("Submodule"); case DeletedMerge: return Tr::tr("Deleted"); case SymbolicLinkMerge: return Tr::tr("Symbolic link"); } return {}; } QString MergeTool::stateName(MergeTool::FileState state, const QString &extraInfo) { switch (state) { case ModifiedState: return Tr::tr("Modified"); case CreatedState: return Tr::tr("Created"); case DeletedState: return Tr::tr("Deleted"); case SubmoduleState: return Tr::tr("Submodule commit %1").arg(extraInfo); case SymbolicLinkState: return Tr::tr("Symbolic link -> %1").arg(extraInfo); default: break; } return {}; } void MergeTool::chooseAction() { if (m_mergeType == NormalMerge) return; QMessageBox msgBox; msgBox.setWindowTitle(Tr::tr("Merge Conflict")); msgBox.setIcon(QMessageBox::Question); msgBox.setStandardButtons(QMessageBox::Abort); msgBox.setText(Tr::tr("%1 merge conflict for \"%2\"\nLocal: %3\nRemote: %4") .arg(mergeTypeName(), m_fileName, stateName(m_localState, m_localInfo), stateName(m_remoteState, m_remoteInfo))); switch (m_mergeType) { case SubmoduleMerge: case SymbolicLinkMerge: addButton(&msgBox, Tr::tr("&Local"), 'l'); addButton(&msgBox, Tr::tr("&Remote"), 'r'); break; case DeletedMerge: if (m_localState == CreatedState || m_remoteState == CreatedState) addButton(&msgBox, Tr::tr("&Created"), 'c'); else addButton(&msgBox, Tr::tr("&Modified"), 'm'); addButton(&msgBox, Tr::tr("&Deleted"), 'd'); break; default: break; } msgBox.exec(); 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('a'); // abort write(QString(key.toChar()) + '\n'); } void MergeTool::addButton(QMessageBox *msgBox, const QString &text, char key) { msgBox->addButton(text, QMessageBox::AcceptRole)->setProperty("key", key); } void MergeTool::prompt(const QString &title, const QString &question) { if (QMessageBox::question(Core::ICore::dialogParent(), title, question, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { write("y\n"); } else { write("n\n"); } } void MergeTool::readData() { QString newData = QString::fromLocal8Bit(m_process.readAllRawStandardOutput()); newData.remove('\r'); VcsOutputWindow::append(newData); QString data = m_unfinishedLine + newData; m_unfinishedLine.clear(); for (;;) { const int index = data.indexOf('\n'); if (index == -1) break; const QString line = data.left(index + 1); readLine(line); data = data.mid(index + 1); } if (data.startsWith("Was the merge successful")) { prompt(Tr::tr("Unchanged File"), Tr::tr("Was the merge successful?")); } else if (data.startsWith("Continue merging")) { prompt(Tr::tr("Continue Merging"), Tr::tr("Continue merging other unresolved paths?")); } else if (data.startsWith("Hit return")) { QMessageBox::warning( Core::ICore::dialogParent(), Tr::tr("Merge Tool"), QString("

%1

\n

%2

").arg( Tr::tr("Merge tool is not configured."), Tr::tr("Run git config --global merge.tool <tool> " "to configure it, then try again."))); m_process.stop(); } else { m_unfinishedLine = data; } } void MergeTool::readLine(const QString &line) { // {Normal|Deleted|Submodule|Symbolic link} merge conflict for 'foo.cpp' const int index = line.indexOf(" merge conflict for "); if (index != -1) { m_mergeType = mergeType(line.left(index)); const int quote = line.indexOf('\''); m_fileName = line.mid(quote + 1, line.lastIndexOf('\'') - quote - 1); } else if (line.startsWith(" {local}")) { m_localState = parseStatus(line, m_localInfo); } else if (line.startsWith(" {remote}")) { m_remoteState = parseStatus(line, m_remoteInfo); chooseAction(); } } void MergeTool::done() { const bool success = m_process.result() == ProcessResult::FinishedWithSuccess; if (success) VcsOutputWindow::appendMessage(m_process.exitMessage()); else VcsOutputWindow::appendError(m_process.exitMessage()); const FilePath workingDirectory = m_process.workingDirectory(); gitClient().continueCommandIfNeeded(workingDirectory, success); emitRepositoryChanged(workingDirectory); deleteLater(); } void MergeTool::write(const QString &str) { m_process.write(str); VcsOutputWindow::append(str); } } // Git::Internal