forked from qt-creator/qt-creator
- missing Q_DECLARE_TR_FUNCTIONS - some full qualifications needed for confused lupdate - avoid breaking some translations from former TaskListPlugin, VcsCommand and highlighter settings refactoring Change-Id: Ia3b34095512a7bad6903f0aff6095313ee39e3e4 Reviewed-by: hjk <hjk@qt.io>
406 lines
13 KiB
C++
406 lines
13 KiB
C++
// Copyright (C) 2016 Brian McGillion and Hugues Delorme
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
|
|
|
#include "vcscommand.h"
|
|
|
|
#include "vcsbaseplugin.h"
|
|
#include "vcsoutputwindow.h"
|
|
|
|
#include <coreplugin/icore.h>
|
|
#include <coreplugin/progressmanager/progressmanager.h>
|
|
|
|
#include <utils/environment.h>
|
|
#include <utils/globalfilechangeblocker.h>
|
|
#include <utils/qtcassert.h>
|
|
#include <utils/qtcprocess.h>
|
|
|
|
#include <QCoreApplication>
|
|
#include <QFuture>
|
|
#include <QFutureWatcher>
|
|
#include <QTextCodec>
|
|
#include <QThread>
|
|
|
|
using namespace Core;
|
|
using namespace Utils;
|
|
|
|
namespace VcsBase {
|
|
namespace Internal {
|
|
|
|
class VcsCommandPrivate : public QObject
|
|
{
|
|
public:
|
|
struct Job {
|
|
CommandLine command;
|
|
int timeoutS = 10;
|
|
FilePath workingDirectory;
|
|
ExitCodeInterpreter exitCodeInterpreter = {};
|
|
};
|
|
|
|
VcsCommandPrivate(VcsCommand *vcsCommand, const FilePath &defaultWorkingDirectory,
|
|
const Environment &environment)
|
|
: q(vcsCommand)
|
|
, m_defaultWorkingDirectory(defaultWorkingDirectory)
|
|
, m_environment(environment)
|
|
{
|
|
VcsBase::setProcessEnvironment(&m_environment);
|
|
m_futureInterface.setProgressRange(0, 1);
|
|
}
|
|
|
|
Environment environment()
|
|
{
|
|
if (!(m_flags & RunFlags::ForceCLocale))
|
|
return m_environment;
|
|
|
|
m_environment.set("LANG", "C");
|
|
m_environment.set("LANGUAGE", "C");
|
|
return m_environment;
|
|
}
|
|
|
|
QString displayName() const;
|
|
int timeoutS() const;
|
|
|
|
void setup();
|
|
void cleanup();
|
|
void setupProcess(QtcProcess *process, const Job &job);
|
|
void installStdCallbacks(QtcProcess *process);
|
|
EventLoopMode eventLoopMode() const;
|
|
void handleDone(QtcProcess *process);
|
|
void startAll();
|
|
void startNextJob();
|
|
void processDone();
|
|
|
|
VcsCommand *q = nullptr;
|
|
|
|
QString m_displayName;
|
|
const FilePath m_defaultWorkingDirectory;
|
|
Environment m_environment;
|
|
QTextCodec *m_codec = nullptr;
|
|
ProgressParser m_progressParser = {};
|
|
QFutureWatcher<void> m_watcher;
|
|
QList<Job> m_jobs;
|
|
|
|
int m_currentJob = 0;
|
|
std::unique_ptr<QtcProcess> m_process;
|
|
QString m_stdOut;
|
|
QString m_stdErr;
|
|
ProcessResult m_result = ProcessResult::StartFailed;
|
|
QFutureInterface<void> m_futureInterface;
|
|
|
|
RunFlags m_flags = RunFlags::None;
|
|
};
|
|
|
|
QString VcsCommandPrivate::displayName() const
|
|
{
|
|
if (!m_displayName.isEmpty())
|
|
return m_displayName;
|
|
if (m_jobs.isEmpty())
|
|
return VcsCommand::tr("UNKNOWN");
|
|
const Job &job = m_jobs.at(0);
|
|
QString result = job.command.executable().baseName();
|
|
if (!result.isEmpty())
|
|
result[0] = result.at(0).toTitleCase();
|
|
else
|
|
result = VcsCommand::tr("UNKNOWN");
|
|
if (!job.command.arguments().isEmpty())
|
|
result += ' ' + job.command.splitArguments().at(0);
|
|
return result;
|
|
}
|
|
|
|
int VcsCommandPrivate::timeoutS() const
|
|
{
|
|
return std::accumulate(m_jobs.cbegin(), m_jobs.cend(), 0,
|
|
[](int sum, const Job &job) { return sum + job.timeoutS; });
|
|
}
|
|
|
|
void VcsCommandPrivate::setup()
|
|
{
|
|
m_futureInterface.reportStarted();
|
|
if (m_flags & RunFlags::ExpectRepoChanges) {
|
|
QMetaObject::invokeMethod(GlobalFileChangeBlocker::instance(), [] {
|
|
GlobalFileChangeBlocker::instance()->forceBlocked(true);
|
|
});
|
|
}
|
|
}
|
|
|
|
void VcsCommandPrivate::cleanup()
|
|
{
|
|
QTC_ASSERT(m_futureInterface.isRunning(), return);
|
|
m_futureInterface.reportFinished();
|
|
if (m_flags & RunFlags::ExpectRepoChanges) {
|
|
QMetaObject::invokeMethod(GlobalFileChangeBlocker::instance(), [] {
|
|
GlobalFileChangeBlocker::instance()->forceBlocked(false);
|
|
});
|
|
}
|
|
}
|
|
|
|
void VcsCommandPrivate::setupProcess(QtcProcess *process, const Job &job)
|
|
{
|
|
process->setExitCodeInterpreter(job.exitCodeInterpreter);
|
|
// TODO: Handle it properly in QtcProcess when QtcProcess::runBlocking() isn't used.
|
|
process->setTimeoutS(job.timeoutS);
|
|
if (!job.workingDirectory.isEmpty())
|
|
process->setWorkingDirectory(job.workingDirectory);
|
|
if (!(m_flags & RunFlags::SuppressCommandLogging))
|
|
emit q->appendCommand(job.workingDirectory, job.command);
|
|
process->setCommand(job.command);
|
|
process->setDisableUnixTerminal();
|
|
process->setEnvironment(environment());
|
|
if (m_flags & RunFlags::MergeOutputChannels)
|
|
process->setProcessChannelMode(QProcess::MergedChannels);
|
|
if (m_codec)
|
|
process->setCodec(m_codec);
|
|
|
|
installStdCallbacks(process);
|
|
}
|
|
|
|
void VcsCommandPrivate::installStdCallbacks(QtcProcess *process)
|
|
{
|
|
if (!(m_flags & RunFlags::MergeOutputChannels) && (m_flags & RunFlags::ProgressiveOutput
|
|
|| !(m_flags & RunFlags::SuppressStdErr))) {
|
|
process->setStdErrCallback([this](const QString &text) {
|
|
if (m_progressParser)
|
|
m_progressParser(m_futureInterface, text);
|
|
if (!(m_flags & RunFlags::SuppressStdErr))
|
|
emit q->appendError(text);
|
|
if (m_flags & RunFlags::ProgressiveOutput)
|
|
emit q->stdErrText(text);
|
|
});
|
|
}
|
|
// connect stdout to the output window if desired
|
|
if (m_progressParser || m_flags & RunFlags::ProgressiveOutput
|
|
|| m_flags & RunFlags::ShowStdOut) {
|
|
process->setStdOutCallback([this](const QString &text) {
|
|
if (m_progressParser)
|
|
m_progressParser(m_futureInterface, text);
|
|
if (m_flags & RunFlags::ShowStdOut) {
|
|
if (m_flags & RunFlags::SilentOutput)
|
|
emit q->appendSilently(text);
|
|
else
|
|
emit q->append(text);
|
|
}
|
|
if (m_flags & RunFlags::ProgressiveOutput)
|
|
emit q->stdOutText(text);
|
|
});
|
|
}
|
|
// TODO: Implement it here
|
|
// m_process->setTimeOutMessageBoxEnabled(true);
|
|
}
|
|
|
|
EventLoopMode VcsCommandPrivate::eventLoopMode() const
|
|
{
|
|
if ((m_flags & RunFlags::UseEventLoop) && QThread::currentThread() == qApp->thread())
|
|
return EventLoopMode::On;
|
|
return EventLoopMode::Off;
|
|
}
|
|
|
|
void VcsCommandPrivate::handleDone(QtcProcess *process)
|
|
{
|
|
// Success/Fail message in appropriate window?
|
|
if (process->result() == ProcessResult::FinishedWithSuccess) {
|
|
if (m_flags & RunFlags::ShowSuccessMessage)
|
|
emit q->appendMessage(process->exitMessage());
|
|
} else if (!(m_flags & RunFlags::SuppressFailMessage)) {
|
|
emit q->appendError(process->exitMessage());
|
|
}
|
|
emit q->runCommandFinished(process->workingDirectory());
|
|
}
|
|
|
|
void VcsCommandPrivate::startAll()
|
|
{
|
|
// Check that the binary path is not empty
|
|
QTC_ASSERT(!m_jobs.isEmpty(), return);
|
|
QTC_ASSERT(!m_process, return);
|
|
setup();
|
|
m_currentJob = 0;
|
|
startNextJob();
|
|
}
|
|
|
|
void VcsCommandPrivate::startNextJob()
|
|
{
|
|
QTC_ASSERT(m_currentJob < m_jobs.count(), return);
|
|
m_process.reset(new QtcProcess);
|
|
connect(m_process.get(), &QtcProcess::done, this, &VcsCommandPrivate::processDone);
|
|
setupProcess(m_process.get(), m_jobs.at(m_currentJob));
|
|
m_process->start();
|
|
}
|
|
|
|
void VcsCommandPrivate::processDone()
|
|
{
|
|
handleDone(m_process.get());
|
|
m_stdOut += m_process->cleanedStdOut();
|
|
m_stdErr += m_process->cleanedStdErr();
|
|
m_result = m_process->result();
|
|
++m_currentJob;
|
|
const bool success = m_process->result() == ProcessResult::FinishedWithSuccess;
|
|
if (m_currentJob < m_jobs.count() && success) {
|
|
m_process.release()->deleteLater();
|
|
startNextJob();
|
|
return;
|
|
}
|
|
emit q->done();
|
|
if (!success)
|
|
m_futureInterface.reportCanceled();
|
|
cleanup();
|
|
// As it is used asynchronously, we need to delete ourselves
|
|
q->deleteLater();
|
|
}
|
|
|
|
} // namespace Internal
|
|
|
|
VcsCommand::VcsCommand(const FilePath &workingDirectory, const Environment &environment) :
|
|
d(new Internal::VcsCommandPrivate(this, workingDirectory, environment))
|
|
{
|
|
connect(&d->m_watcher, &QFutureWatcher<void>::canceled, this, &VcsCommand::cancel);
|
|
|
|
VcsOutputWindow::setRepository(d->m_defaultWorkingDirectory);
|
|
VcsOutputWindow *outputWindow = VcsOutputWindow::instance(); // Keep me here, just to be sure it's not instantiated in other thread
|
|
connect(this, &VcsCommand::append, outputWindow, [outputWindow](const QString &t) {
|
|
outputWindow->append(t);
|
|
});
|
|
connect(this, &VcsCommand::appendSilently, outputWindow, &VcsOutputWindow::appendSilently);
|
|
connect(this, &VcsCommand::appendError, outputWindow, &VcsOutputWindow::appendError);
|
|
connect(this, &VcsCommand::appendCommand, outputWindow, &VcsOutputWindow::appendCommand);
|
|
connect(this, &VcsCommand::appendMessage, outputWindow, &VcsOutputWindow::appendMessage);
|
|
const auto connection = connect(this, &VcsCommand::runCommandFinished,
|
|
this, &VcsCommand::postRunCommand);
|
|
connect(ICore::instance(), &ICore::coreAboutToClose, this, [this, connection] {
|
|
disconnect(connection);
|
|
d->m_process.reset();
|
|
if (d->m_futureInterface.isRunning()) {
|
|
d->m_futureInterface.reportCanceled();
|
|
d->cleanup();
|
|
}
|
|
});
|
|
}
|
|
|
|
void VcsCommand::postRunCommand(const FilePath &workingDirectory)
|
|
{
|
|
if (!(d->m_flags & RunFlags::ExpectRepoChanges))
|
|
return;
|
|
// TODO tell the document manager that the directory now received all expected changes
|
|
// Core::DocumentManager::unexpectDirectoryChange(d->m_workingDirectory);
|
|
VcsManager::emitRepositoryChanged(workingDirectory);
|
|
}
|
|
|
|
VcsCommand::~VcsCommand()
|
|
{
|
|
if (d->m_futureInterface.isRunning()) {
|
|
d->m_futureInterface.reportCanceled();
|
|
d->cleanup();
|
|
}
|
|
delete d;
|
|
}
|
|
|
|
void VcsCommand::setDisplayName(const QString &name)
|
|
{
|
|
d->m_displayName = name;
|
|
}
|
|
|
|
void VcsCommand::addFlags(RunFlags f)
|
|
{
|
|
d->m_flags |= f;
|
|
}
|
|
|
|
void VcsCommand::addJob(const CommandLine &command, int timeoutS,
|
|
const FilePath &workingDirectory,
|
|
const ExitCodeInterpreter &interpreter)
|
|
{
|
|
QTC_ASSERT(!command.executable().isEmpty(), return);
|
|
d->m_jobs.push_back({command, timeoutS, !workingDirectory.isEmpty()
|
|
? workingDirectory : d->m_defaultWorkingDirectory, interpreter});
|
|
}
|
|
|
|
void VcsCommand::start()
|
|
{
|
|
if (d->m_jobs.empty())
|
|
return;
|
|
|
|
d->startAll();
|
|
d->m_watcher.setFuture(d->m_futureInterface.future());
|
|
if ((d->m_flags & RunFlags::SuppressCommandLogging))
|
|
return;
|
|
|
|
const QString name = d->displayName();
|
|
const auto id = Id::fromString(name + QLatin1String(".action"));
|
|
if (d->m_progressParser)
|
|
ProgressManager::addTask(d->m_futureInterface.future(), name, id);
|
|
else
|
|
ProgressManager::addTimedTask(d->m_futureInterface, name, id, qMax(2, d->timeoutS() / 5));
|
|
}
|
|
|
|
void VcsCommand::cancel()
|
|
{
|
|
d->m_futureInterface.reportCanceled();
|
|
if (d->m_process) {
|
|
// TODO: we may want to call cancel here...
|
|
d->m_process->stop();
|
|
// TODO: we may want to not wait here...
|
|
d->m_process->waitForFinished();
|
|
d->m_process.reset();
|
|
}
|
|
}
|
|
|
|
QString VcsCommand::cleanedStdOut() const
|
|
{
|
|
return d->m_stdOut;
|
|
}
|
|
|
|
QString VcsCommand::cleanedStdErr() const
|
|
{
|
|
return d->m_stdErr;
|
|
}
|
|
|
|
ProcessResult VcsCommand::result() const
|
|
{
|
|
return d->m_result;
|
|
}
|
|
|
|
CommandResult VcsCommand::runBlocking(const Utils::FilePath &workingDirectory,
|
|
const Utils::Environment &environment,
|
|
const Utils::CommandLine &command, RunFlags flags,
|
|
int timeoutS, QTextCodec *codec)
|
|
{
|
|
VcsCommand vcsCommand(workingDirectory, environment);
|
|
vcsCommand.addFlags(flags);
|
|
vcsCommand.setCodec(codec);
|
|
return vcsCommand.runBlockingHelper(command, timeoutS);
|
|
}
|
|
|
|
CommandResult VcsCommand::runBlockingHelper(const CommandLine &command, int timeoutS)
|
|
{
|
|
QtcProcess process;
|
|
if (command.executable().isEmpty())
|
|
return {};
|
|
|
|
d->setupProcess(&process, {command, timeoutS, d->m_defaultWorkingDirectory, {}});
|
|
|
|
const EventLoopMode eventLoopMode = d->eventLoopMode();
|
|
process.setTimeOutMessageBoxEnabled(eventLoopMode == EventLoopMode::On);
|
|
process.runBlocking(eventLoopMode);
|
|
d->handleDone(&process);
|
|
|
|
return CommandResult(process);
|
|
}
|
|
|
|
void VcsCommand::setCodec(QTextCodec *codec)
|
|
{
|
|
d->m_codec = codec;
|
|
}
|
|
|
|
//! Use \a parser to parse progress data from stdout. Command takes ownership of \a parser
|
|
void VcsCommand::setProgressParser(const ProgressParser &parser)
|
|
{
|
|
d->m_progressParser = parser;
|
|
}
|
|
|
|
CommandResult::CommandResult(const QtcProcess &process)
|
|
: m_result(process.result())
|
|
, m_exitCode(process.exitCode())
|
|
, m_exitMessage(process.exitMessage())
|
|
, m_cleanedStdOut(process.cleanedStdOut())
|
|
, m_cleanedStdErr(process.cleanedStdErr())
|
|
, m_rawStdOut(process.rawStdOut())
|
|
{}
|
|
|
|
} // namespace VcsBase
|