/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
**
** GNU Lesser General Public License Usage
**
** 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, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** 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.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**************************************************************************/

#include "vcsbaseplugin.h"
#include "vcsbasesubmiteditor.h"
#include "vcsplugin.h"
#include "commonvcssettings.h"
#include "vcsbaseoutputwindow.h"
#include "corelistener.h"

#include <coreplugin/icore.h>
#include <coreplugin/ifile.h>
#include <coreplugin/iversioncontrol.h>
#include <coreplugin/filemanager.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/vcsmanager.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/project.h>
#include <utils/qtcassert.h>
#include <utils/synchronousprocess.h>
#include <utils/environment.h>

#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QSharedData>
#include <QtCore/QScopedPointer>
#include <QtCore/QSharedPointer>
#include <QtCore/QProcessEnvironment>
#include <QtCore/QTextStream>
#include <QtCore/QTextCodec>

#include <QtGui/QAction>
#include <QtGui/QMessageBox>
#include <QtGui/QFileDialog>
#include <QtGui/QMainWindow>

enum { debug = 0, debugRepositorySearch = 0, debugExecution = 0 };

/*!
    \namespace VCSBase
    \brief VCSBase plugin namespace
*/

/*!
    \namespace VCSBase::Internal
    \brief Internal namespace of the VCSBase plugin
    \internal
*/

namespace VCSBase {
namespace Internal {

/*!
    \struct VCSBase::Internal::State

    \brief Internal state created by the state listener and VCSBasePluginState.

    Aggregated in the QSharedData of VCSBase::VCSBasePluginState.
*/

struct State {
    void clearFile();
    void clearPatchFile();
    void clearProject();
    inline void clear();

    bool equals(const State &rhs) const;

    inline bool hasFile() const     { return !currentFile.isEmpty(); }
    inline bool hasProject() const  { return !currentProjectPath.isEmpty(); }
    inline bool isEmpty() const     { return !hasFile() && !hasProject(); }

    QString currentFile;
    QString currentFileName;
    QString currentPatchFile;
    QString currentPatchFileDisplayName;

    QString currentFileDirectory;
    QString currentFileTopLevel;

    QString currentProjectPath;
    QString currentProjectName;
    QString currentProjectTopLevel;
};

void State::clearFile()
{
    currentFile.clear();
    currentFileName.clear();
    currentFileDirectory.clear();
    currentFileTopLevel.clear();
}

void State::clearPatchFile()
{
    currentPatchFile.clear();
    currentPatchFileDisplayName.clear();
}

void State::clearProject()
{
    currentProjectPath.clear();
    currentProjectName.clear();
    currentProjectTopLevel.clear();
}

void State::clear()
{
    clearFile();
    clearPatchFile();
    clearProject();
}

bool State::equals(const State &rhs) const
{
    return currentFile == rhs.currentFile
            && currentFileName == rhs.currentFileName
            && currentPatchFile == rhs.currentPatchFile
            && currentPatchFileDisplayName == rhs.currentPatchFileDisplayName
            && currentFileTopLevel == rhs.currentFileTopLevel
            && currentProjectPath == rhs.currentProjectPath
            && currentProjectName == rhs.currentProjectName
            && currentProjectTopLevel == rhs.currentProjectTopLevel;
}

QDebug operator<<(QDebug in, const State &state)
{
    QDebug nospace = in.nospace();
    nospace << "State: ";
    if (state.isEmpty()) {
        nospace << "<empty>";
    } else {
        if (state.hasFile()) {
            nospace << "File=" << state.currentFile
                    << ',' << state.currentFileTopLevel;
        } else {
            nospace << "<no file>";
        }
        nospace << '\n';
        if (state.hasProject()) {
            nospace << "       Project=" << state.currentProjectName
            << ',' << state.currentProjectPath
            << ',' << state.currentProjectTopLevel;

        } else {
            nospace << "<no project>";
        }
        nospace << '\n';
    }
    return in;
}

/*!
    \class VCSBase::Internal::StateListener

    \brief Connects to the relevant signals of Qt Creator, tries to find version
    controls and emits signals to the plugin instances.

    Singleton (as not to do checks multiple times).
*/

class StateListener : public QObject {
    Q_OBJECT
public:
    explicit StateListener(QObject *parent);

signals:
    void stateChanged(const VCSBase::Internal::State &s, Core::IVersionControl *vc);

public slots:
    void slotStateChanged();
};

StateListener::StateListener(QObject *parent) :
        QObject(parent)
{
    Core::ICore *core = Core::ICore::instance();
    connect(core->editorManager(), SIGNAL(currentEditorChanged(Core::IEditor*)),
            this, SLOT(slotStateChanged()));
    connect(core->editorManager(), SIGNAL(currentEditorStateChanged(Core::IEditor*)),
            this, SLOT(slotStateChanged()));
    connect(core->vcsManager(), SIGNAL(repositoryChanged(QString)),
            this, SLOT(slotStateChanged()));

    if (ProjectExplorer::ProjectExplorerPlugin *pe = ProjectExplorer::ProjectExplorerPlugin::instance())
        connect(pe, SIGNAL(currentProjectChanged(ProjectExplorer::Project*)),
                this, SLOT(slotStateChanged()));
}

static inline QString displayNameOfEditor(const QString &fileName)
{
    const QList<Core::IEditor*> editors = Core::EditorManager::instance()->editorsForFileName(fileName);
    if (!editors.isEmpty())
        return editors.front()->displayName();
    return QString();
}

void StateListener::slotStateChanged()
{
    const ProjectExplorer::ProjectExplorerPlugin *pe = ProjectExplorer::ProjectExplorerPlugin::instance();
    const Core::ICore *core = Core::ICore::instance();
    Core::VcsManager *vcsManager = core->vcsManager();

    // Get the current file. Are we on a temporary submit editor indicated by
    // temporary path prefix or does the file contains a hash, indicating a project
    // folder?
    State state;
    Core::EditorManager *em = core->editorManager();
    if (!em || !em->currentEditor() || !em->currentEditor()->file())
        state.currentFile.clear();
    else
        state.currentFile = em->currentEditor()->file()->fileName();
    QScopedPointer<QFileInfo> currentFileInfo; // Instantiate QFileInfo only once if required.
    if (!state.currentFile.isEmpty()) {
        const bool isTempFile = state.currentFile.startsWith(QDir::tempPath());
        // Quick check: Does it look like a patch?
        const bool isPatch = state.currentFile.endsWith(QLatin1String(".patch"))
                             || state.currentFile.endsWith(QLatin1String(".diff"));
        if (isPatch) {
            // Patch: Figure out a name to display. If it is a temp file, it could be
            // Codepaster. Use the display name of the editor.
            state.currentPatchFile = state.currentFile;
            if (isTempFile)
                state.currentPatchFileDisplayName = displayNameOfEditor(state.currentPatchFile);
            if (state.currentPatchFileDisplayName.isEmpty()) {
                currentFileInfo.reset(new QFileInfo(state.currentFile));
                state.currentPatchFileDisplayName = currentFileInfo->fileName();
            }
        }
        // For actual version control operations on it:
        // Do not show temporary files and project folders ('#')
        if (isTempFile || state.currentFile.contains(QLatin1Char('#')))
            state.currentFile.clear();
    }

    // Get the file and its control. Do not use the file unless we find one
    Core::IVersionControl *fileControl = 0;
    if (!state.currentFile.isEmpty()) {
        if (currentFileInfo.isNull())
            currentFileInfo.reset(new QFileInfo(state.currentFile));
        state.currentFileDirectory = currentFileInfo->absolutePath();
        state.currentFileName = currentFileInfo->fileName();
        fileControl = vcsManager->findVersionControlForDirectory(state.currentFileDirectory,
                                                                 &state.currentFileTopLevel);
        if (!fileControl)
            state.clearFile();
    }
    // Check for project, find the control
    Core::IVersionControl *projectControl = 0;
    if (const ProjectExplorer::Project *currentProject = pe->currentProject()) {
        state.currentProjectPath = currentProject->projectDirectory();
        state.currentProjectName = currentProject->displayName();
        projectControl = vcsManager->findVersionControlForDirectory(state.currentProjectPath,
                                                                    &state.currentProjectTopLevel);
        if (projectControl) {
            // If we have both, let the file's one take preference
            if (fileControl && projectControl != fileControl) {
                state.clearProject();
            }
        } else {
            state.clearProject(); // No control found
        }
    }
    // Assemble state and emit signal.
    Core::IVersionControl *vc = state.currentFile.isEmpty() ? projectControl : fileControl;
    if (!vc) // Need a repository to patch
        state.clearPatchFile();
    if (debug)
        qDebug() << state << (vc ? vc->displayName() : QString(QLatin1String("No version control")));
    emit stateChanged(state, vc);
}

} // namespace Internal

class VCSBasePluginStateData : public QSharedData {
public:
    Internal::State m_state;
};

/*!
    \class  VCSBase::VCSBasePluginState

    \brief Relevant state information of the VCS plugins

    Qt Creator's state relevant to VCS plugins is a tuple of

    \list
    \o Current file and it's version system control/top level
    \o Current project and it's version system control/top level
    \endlist

    \sa VCSBase::VCSBasePlugin
*/

VCSBasePluginState::VCSBasePluginState() : data(new VCSBasePluginStateData)
{
}

VCSBasePluginState::VCSBasePluginState(const VCSBasePluginState &rhs) : data(rhs.data)
{
}

VCSBasePluginState &VCSBasePluginState::operator=(const VCSBasePluginState &rhs)
{
    if (this != &rhs)
        data.operator=(rhs.data);
    return *this;
}

VCSBasePluginState::~VCSBasePluginState()
{
}

QString VCSBasePluginState::currentFile() const
{
    return data->m_state.currentFile;
}

QString VCSBasePluginState::currentFileName() const
{
    return data->m_state.currentFileName;
}

QString VCSBasePluginState::currentFileTopLevel() const
{
    return data->m_state.currentFileTopLevel;
}

QString VCSBasePluginState::currentFileDirectory() const
{
    return data->m_state.currentFileDirectory;
}

QString VCSBasePluginState::relativeCurrentFile() const
{
    QTC_ASSERT(hasFile(), return QString())
    return QDir(data->m_state.currentFileTopLevel).relativeFilePath(data->m_state.currentFile);
}

QString VCSBasePluginState::currentPatchFile() const
{
    return data->m_state.currentPatchFile;
}

QString VCSBasePluginState::currentPatchFileDisplayName() const
{
    return data->m_state.currentPatchFileDisplayName;
}

QString VCSBasePluginState::currentProjectPath() const
{
    return data->m_state.currentProjectPath;
}

QString VCSBasePluginState::currentProjectName() const
{
    return data->m_state.currentProjectName;
}

QString VCSBasePluginState::currentProjectTopLevel() const
{
    return data->m_state.currentProjectTopLevel;
}

QStringList VCSBasePluginState::relativeCurrentProject() const
{
    QStringList rc;
    QTC_ASSERT(hasProject(), return rc)
    if (data->m_state.currentProjectTopLevel != data->m_state.currentProjectPath)
        rc.append(QDir(data->m_state.currentProjectTopLevel).relativeFilePath(data->m_state.currentProjectPath));
    return rc;
}

bool VCSBasePluginState::hasTopLevel() const
{
    return data->m_state.hasFile() || data->m_state.hasProject();
}

QString VCSBasePluginState::topLevel() const
{
    return hasFile() ? data->m_state.currentFileTopLevel : data->m_state.currentProjectTopLevel;
}

bool VCSBasePluginState::equals(const Internal::State &rhs) const
{
    return data->m_state.equals(rhs);
}

bool VCSBasePluginState::equals(const VCSBasePluginState &rhs) const
{
    return equals(rhs.data->m_state);
}

void VCSBasePluginState::clear()
{
    data->m_state.clear();
}

void VCSBasePluginState::setState(const Internal::State &s)
{
    data->m_state = s;
}

bool VCSBasePluginState::isEmpty() const
{
    return data->m_state.isEmpty();
}

bool VCSBasePluginState::hasFile() const
{
    return data->m_state.hasFile();
}

bool VCSBasePluginState::hasPatchFile() const
{
    return !data->m_state.currentPatchFile.isEmpty();
}

bool VCSBasePluginState::hasProject() const
{
    return data->m_state.hasProject();
}

VCSBASE_EXPORT QDebug operator<<(QDebug in, const VCSBasePluginState &state)
{
    in << state.data->m_state;
    return in;
}

/*!
    \class VCSBase::VCSBasePlugin

    \brief Base class for all version control plugins.

    The plugin connects to the
    relevant change signals in Qt Creator and calls the virtual
    updateActions() for the plugins to update their menu actions
    according to the new state. This is done centrally to avoid
    single plugins repeatedly invoking searches/QFileInfo on files,
    etc.

    Independently, there are accessors for current patch files, which return
    a file name if the current file could be a patch file which could be applied
    and a repository exists.

    If current file/project are managed
    by different version controls, the project is discarded and only
    the current file is taken into account, allowing to do a diff
    also when the project of a file is not opened.

    When triggering an action, a copy of the state should be made to
    keep it, as it may rapidly change due to context changes, etc.

    The class also detects the VCS plugin submit editor closing and calls
    the virtual submitEditorAboutToClose() to trigger the submit process.
*/

struct VCSBasePluginPrivate {
    explicit VCSBasePluginPrivate(const QString &submitEditorId);

    inline bool supportsRepositoryCreation() const;

    const QString m_submitEditorId;
    Core::IVersionControl *m_versionControl;
    VCSBasePluginState m_state;
    int m_actionState;
    QAction *m_testSnapshotAction;
    QAction *m_testListSnapshotsAction;
    QAction *m_testRestoreSnapshotAction;
    QAction *m_testRemoveSnapshotAction;
    QString m_testLastSnapshot;

    static Internal::StateListener *m_listener;
};

VCSBasePluginPrivate::VCSBasePluginPrivate(const QString &submitEditorId) :
    m_submitEditorId(submitEditorId),
    m_versionControl(0),
    m_actionState(-1),
    m_testSnapshotAction(0),
    m_testListSnapshotsAction(0),
    m_testRestoreSnapshotAction(0),
    m_testRemoveSnapshotAction(0)
{
}

bool VCSBasePluginPrivate::supportsRepositoryCreation() const
{
    return m_versionControl && m_versionControl->supportsOperation(Core::IVersionControl::CreateRepositoryOperation);
}

Internal::StateListener *VCSBasePluginPrivate::m_listener = 0;

VCSBasePlugin::VCSBasePlugin(const QString &submitEditorId) :
    d(new VCSBasePluginPrivate(submitEditorId))
{
}

VCSBasePlugin::~VCSBasePlugin()
{
    delete d;
}

void VCSBasePlugin::initializeVcs(Core::IVersionControl *vc)
{
    d->m_versionControl = vc;
    addAutoReleasedObject(vc);

    Internal::VCSPlugin *plugin = Internal::VCSPlugin::instance();
    connect(plugin->coreListener(), SIGNAL(submitEditorAboutToClose(VCSBaseSubmitEditor*,bool*)),
            this, SLOT(slotSubmitEditorAboutToClose(VCSBaseSubmitEditor*,bool*)));
    // First time: create new listener
    if (!VCSBasePluginPrivate::m_listener)
        VCSBasePluginPrivate::m_listener = new Internal::StateListener(plugin);
    connect(VCSBasePluginPrivate::m_listener,
            SIGNAL(stateChanged(VCSBase::Internal::State, Core::IVersionControl*)),
            this,
            SLOT(slotStateChanged(VCSBase::Internal::State,Core::IVersionControl*)));
}

void VCSBasePlugin::extensionsInitialized()
{
    // Initialize enable menus.
    VCSBasePluginPrivate::m_listener->slotStateChanged();
}

void VCSBasePlugin::slotSubmitEditorAboutToClose(VCSBaseSubmitEditor *submitEditor, bool *result)
{
    if (debug)
        qDebug() << this << d->m_submitEditorId << "Closing submit editor" << submitEditor << submitEditor->id();
    if (submitEditor->id() == d->m_submitEditorId)
        *result = submitEditorAboutToClose(submitEditor);
}

Core::IVersionControl *VCSBasePlugin::versionControl() const
{
    return d->m_versionControl;
}

void VCSBasePlugin::slotStateChanged(const VCSBase::Internal::State &newInternalState, Core::IVersionControl *vc)
{
    if (vc == d->m_versionControl) {
        // We are directly affected: Change state
        if (!d->m_state.equals(newInternalState)) {
            d->m_state.setState(newInternalState);
            updateActions(VCSEnabled);
        }
    } else {
        // Some other VCS plugin or state changed: Reset us to empty state.
        const ActionState newActionState = vc ? OtherVCSEnabled : NoVCSEnabled;
        if (d->m_actionState != newActionState || !d->m_state.isEmpty()) {
            d->m_actionState = newActionState;
            const VCSBasePluginState emptyState;
            d->m_state = emptyState;
            updateActions(newActionState);
        }
    }
}

const VCSBasePluginState &VCSBasePlugin::currentState() const
{
    return d->m_state;
}

bool VCSBasePlugin::enableMenuAction(ActionState as, QAction *menuAction) const
{
    if (debug)
        qDebug() << "enableMenuAction" << menuAction->text() << as;
    switch (as) {
    case VCSBase::VCSBasePlugin::NoVCSEnabled: {
        const bool supportsCreation = d->supportsRepositoryCreation();
        menuAction->setVisible(supportsCreation);
        menuAction->setEnabled(supportsCreation);
        return supportsCreation;
    }
    case VCSBase::VCSBasePlugin::OtherVCSEnabled:
        menuAction->setVisible(false);
        return false;
    case VCSBase::VCSBasePlugin::VCSEnabled:
        menuAction->setVisible(true);
        menuAction->setEnabled(true);
        break;
    }
    return true;
}

void VCSBasePlugin::promptToDeleteCurrentFile()
{
    const VCSBasePluginState state = currentState();
    QTC_ASSERT(state.hasFile(), return)
    const bool rc = Core::ICore::instance()->vcsManager()->promptToDelete(versionControl(), state.currentFile());
    if (!rc)
        QMessageBox::warning(0, tr("Version Control"),
                             tr("The file '%1' could not be deleted.").
                             arg(QDir::toNativeSeparators(state.currentFile())),
                             QMessageBox::Ok);
}

static inline bool ask(QWidget *parent, const QString &title, const QString &question, bool defaultValue = true)

{
    const QMessageBox::StandardButton defaultButton = defaultValue ? QMessageBox::Yes : QMessageBox::No;
    return QMessageBox::question(parent, title, question, QMessageBox::Yes|QMessageBox::No, defaultButton) == QMessageBox::Yes;
}

void VCSBasePlugin::createRepository()
{
    QTC_ASSERT(d->m_versionControl->supportsOperation(Core::IVersionControl::CreateRepositoryOperation), return);
    // Find current starting directory
    QString directory;
    if (const ProjectExplorer::Project *currentProject = ProjectExplorer::ProjectExplorerPlugin::instance()->currentProject())
        directory = QFileInfo(currentProject->file()->fileName()).absolutePath();
    // Prompt for a directory that is not under version control yet
    QMainWindow *mw = Core::ICore::instance()->mainWindow();
    do {
        directory = QFileDialog::getExistingDirectory(mw, tr("Choose Repository Directory"), directory);
        if (directory.isEmpty())
            return;
        const Core::IVersionControl *managingControl = Core::ICore::instance()->vcsManager()->findVersionControlForDirectory(directory);
        if (managingControl == 0)
            break;
        const QString question = tr("The directory '%1' is already managed by a version control system (%2)."
                                    " Would you like to specify another directory?").arg(directory, managingControl->displayName());

        if (!ask(mw, tr("Repository already under version control"), question))
            return;
    } while (true);
    // Create
    const bool rc = d->m_versionControl->vcsCreateRepository(directory);
    const QString nativeDir = QDir::toNativeSeparators(directory);
    if (rc) {
        QMessageBox::information(mw, tr("Repository Created"),
                                 tr("A version control repository has been created in %1.").
                                 arg(nativeDir));
    } else {
        QMessageBox::warning(mw, tr("Repository Creation Failed"),
                                 tr("A version control repository could not be created in %1.").
                                 arg(nativeDir));
    }
}

// For internal tests: Create actions driving IVersionControl's snapshot interface.
QList<QAction*> VCSBasePlugin::createSnapShotTestActions()
{
    if (!d->m_testSnapshotAction) {
        d->m_testSnapshotAction = new QAction(QLatin1String("Take snapshot"), this);
        connect(d->m_testSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestSnapshot()));
        d->m_testListSnapshotsAction = new QAction(QLatin1String("List snapshots"), this);
        connect(d->m_testListSnapshotsAction, SIGNAL(triggered()), this, SLOT(slotTestListSnapshots()));
        d->m_testRestoreSnapshotAction = new QAction(QLatin1String("Restore snapshot"), this);
        connect(d->m_testRestoreSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestRestoreSnapshot()));
        d->m_testRemoveSnapshotAction = new QAction(QLatin1String("Remove snapshot"), this);
        connect(d->m_testRemoveSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestRemoveSnapshot()));
    }
    QList<QAction*> rc;
    rc << d->m_testSnapshotAction << d->m_testListSnapshotsAction << d->m_testRestoreSnapshotAction
       << d->m_testRemoveSnapshotAction;
    return rc;
}

void VCSBasePlugin::slotTestSnapshot()
{
    QTC_ASSERT(currentState().hasTopLevel(), return)
    d->m_testLastSnapshot = versionControl()->vcsCreateSnapshot(currentState().topLevel());
    qDebug() << "Snapshot " << d->m_testLastSnapshot;
    VCSBaseOutputWindow::instance()->append(QLatin1String("Snapshot: ") + d->m_testLastSnapshot);
    if (!d->m_testLastSnapshot.isEmpty())
        d->m_testRestoreSnapshotAction->setText(QLatin1String("Restore snapshot ") + d->m_testLastSnapshot);
}

void VCSBasePlugin::slotTestListSnapshots()
{
    QTC_ASSERT(currentState().hasTopLevel(), return)
    const QStringList snapshots = versionControl()->vcsSnapshots(currentState().topLevel());
    qDebug() << "Snapshots " << snapshots;
    VCSBaseOutputWindow::instance()->append(QLatin1String("Snapshots: ") + snapshots.join(QLatin1String(", ")));
}

void VCSBasePlugin::slotTestRestoreSnapshot()
{
    QTC_ASSERT(currentState().hasTopLevel() && !d->m_testLastSnapshot.isEmpty(), return)
    const bool ok = versionControl()->vcsRestoreSnapshot(currentState().topLevel(), d->m_testLastSnapshot);
    const QString msg = d->m_testLastSnapshot+ (ok ? QLatin1String(" restored") : QLatin1String(" failed"));
    qDebug() << msg;
    VCSBaseOutputWindow::instance()->append(msg);
}

void VCSBasePlugin::slotTestRemoveSnapshot()
{
    QTC_ASSERT(currentState().hasTopLevel() && !d->m_testLastSnapshot.isEmpty(), return)
    const bool ok = versionControl()->vcsRemoveSnapshot(currentState().topLevel(), d->m_testLastSnapshot);
    const QString msg = d->m_testLastSnapshot+ (ok ? QLatin1String(" removed") : QLatin1String(" failed"));
    qDebug() << msg;
    VCSBaseOutputWindow::instance()->append(msg);
    d->m_testLastSnapshot.clear();
}

// Find top level for version controls like git/Mercurial that have
// a directory at the top of the repository.
// Note that checking for the existence of files is preferred over directories
// since checking for directories can cause them to be created when
// AutoFS is used (due its automatically creating mountpoints when querying
// a directory). In addition, bail out when reaching the home directory
// of the user or root (generally avoid '/', where mountpoints are created).
QString VCSBasePlugin::findRepositoryForDirectory(const QString &dirS,
                                                  const QString &checkFile)
{
    if (debugRepositorySearch)
        qDebug() << ">VCSBasePlugin::findRepositoryForDirectory" << dirS << checkFile;
    QTC_ASSERT(!dirS.isEmpty() && !checkFile.isEmpty(), return QString());

    const QString root = QDir::rootPath();
    const QString home = QDir::homePath();

    QDir directory(dirS);
    do {
        const QString absDirPath = directory.absolutePath();
        if (absDirPath == root || absDirPath == home)
            break;

        if (QFileInfo(directory, checkFile).isFile()) {
            if (debugRepositorySearch)
                qDebug() << "<VCSBasePlugin::findRepositoryForDirectory> " << absDirPath;
            return absDirPath;
        }
    } while (directory.cdUp());
    if (debugRepositorySearch)
        qDebug() << "<VCSBasePlugin::findRepositoryForDirectory bailing out at " << directory.absolutePath();
    return QString();
}

// Is SSH prompt configured?
static inline QString sshPrompt()
{
    return VCSBase::Internal::VCSPlugin::instance()->settings().sshPasswordPrompt;
}

bool VCSBasePlugin::isSshPromptConfigured()
{
    return !sshPrompt().isEmpty();
}

void VCSBasePlugin::setProcessEnvironment(QProcessEnvironment *e, bool forceCLocale)
{
    if (forceCLocale)
        e->insert(QLatin1String("LANG"), QString(QLatin1Char('C')));
    const QString sshPromptBinary = sshPrompt();
    if (!sshPromptBinary.isEmpty())
        e->insert(QLatin1String("SSH_ASKPASS"), sshPromptBinary);
}

// Run a process fully synchronously, returning Utils::SynchronousProcessResponse
// response struct and using the VCSBasePlugin flags as applicable
static Utils::SynchronousProcessResponse
    runVCS_FullySynchronously(const QString &workingDir,
                              const QString &binary,
                              const QStringList &arguments,
                              int timeOutMS,
                              QProcessEnvironment env,
                              unsigned flags,
                              QTextCodec *outputCodec = 0)
{
    VCSBase::VCSBaseOutputWindow *outputWindow = VCSBase::VCSBaseOutputWindow::instance();

    // Set up process
    unsigned processFlags = 0;
    if (VCSBasePlugin::isSshPromptConfigured() && (flags & VCSBasePlugin::SshPasswordPrompt))
        processFlags |= Utils::SynchronousProcess::UnixTerminalDisabled;
    QSharedPointer<QProcess> process = Utils::SynchronousProcess::createProcess(processFlags);
    if (!workingDir.isEmpty())
        process->setWorkingDirectory(workingDir);
    process->setProcessEnvironment(env);
    if (flags & VCSBasePlugin::MergeOutputChannels)
        process->setProcessChannelMode(QProcess::MergedChannels);

    // Start
    process->start(binary, arguments, QIODevice::ReadOnly);
    process->closeWriteChannel();
    Utils::SynchronousProcessResponse response;
    if (!process->waitForStarted()) {
        response.result = Utils::SynchronousProcessResponse::StartFailed;
        return response;
    }

    // process output
    QByteArray stdOut;
    QByteArray stdErr;
    const bool timedOut =
            !Utils::SynchronousProcess::readDataFromProcess(*process.data(), timeOutMS,
                                                            &stdOut, &stdErr, true);

    if (!stdErr.isEmpty()) {
        response.stdErr = QString::fromLocal8Bit(stdErr).remove('\r');
        if (!(flags & VCSBasePlugin::SuppressStdErrInLogWindow))
            outputWindow->append(response.stdErr);
    }

    if (!stdOut.isEmpty()) {
        response.stdOut = (outputCodec ? outputCodec->toUnicode(stdOut) : QString::fromLocal8Bit(stdOut))
                          .remove('\r');
        if (flags & VCSBasePlugin::ShowStdOutInLogWindow)
            outputWindow->append(response.stdOut);
    }

    // Result
    if (timedOut) {
        response.result = Utils::SynchronousProcessResponse::Hang;
    } else if (process->exitStatus() != QProcess::NormalExit) {
        response.result = Utils::SynchronousProcessResponse::TerminatedAbnormally;
    } else {
        response.result = process->exitCode() == 0 ?
                          Utils::SynchronousProcessResponse::Finished :
                          Utils::SynchronousProcessResponse::FinishedError;
    }
    return response;
}


Utils::SynchronousProcessResponse
VCSBasePlugin::runVCS(const QString &workingDir,
                      const QString &binary,
                      const QStringList &arguments,
                      int timeOutMS,
                      unsigned flags,
                      QTextCodec *outputCodec)
{
    const QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    return runVCS(workingDir, binary, arguments, timeOutMS, env,
                  flags, outputCodec);
}

Utils::SynchronousProcessResponse VCSBasePlugin::runVCS(const QString &workingDir,
                                                        const QString &binary,
                                                        const QStringList &arguments,
                                                        int timeOutMS,
                                                        QProcessEnvironment env,
                                                        unsigned flags,
                                                        QTextCodec *outputCodec)
{
    VCSBase::VCSBaseOutputWindow *outputWindow = VCSBase::VCSBaseOutputWindow::instance();

    if (!(flags & SuppressCommandLogging))
        outputWindow->appendCommand(workingDir, binary, arguments);

    const bool sshPromptConfigured = VCSBasePlugin::isSshPromptConfigured();
    if (debugExecution) {
        QDebug nsp = qDebug().nospace();
        nsp << "VCSBasePlugin::runVCS" << workingDir << binary << arguments
                << timeOutMS;
        if (flags & ShowStdOutInLogWindow)
            nsp << "stdout";
        if (flags & SuppressStdErrInLogWindow)
            nsp << "suppress_stderr";
        if (flags & SuppressFailMessageInLogWindow)
            nsp << "suppress_fail_msg";
        if (flags & MergeOutputChannels)
            nsp << "merge_channels";
        if (flags & SshPasswordPrompt)
            nsp << "ssh (" << sshPromptConfigured << ')';
        if (flags & SuppressCommandLogging)
            nsp << "suppress_log";
        if (flags & ForceCLocale)
            nsp << "c_locale";
        if (flags & FullySynchronously)
            nsp << "fully_synchronously";
        if (outputCodec)
            nsp << " Codec: " << outputCodec->name();
    }

    VCSBase::VCSBasePlugin::setProcessEnvironment(&env, (flags & ForceCLocale));

    Utils::SynchronousProcessResponse response;

    if (flags & FullySynchronously) {
        response = runVCS_FullySynchronously(workingDir, binary, arguments, timeOutMS,
                                             env, flags, outputCodec);
    } else {
        // Run, connect stderr to the output window
        Utils::SynchronousProcess process;
        if (!workingDir.isEmpty())
            process.setWorkingDirectory(workingDir);

        process.setProcessEnvironment(env);
        process.setTimeout(timeOutMS);
        if (outputCodec)
            process.setStdOutCodec(outputCodec);

        // Suppress terminal on UNIX for ssh prompts if it is configured.
        if (sshPromptConfigured && (flags & SshPasswordPrompt))
            process.setFlags(Utils::SynchronousProcess::UnixTerminalDisabled);

        // connect stderr to the output window if desired
        if (flags & MergeOutputChannels) {
            process.setProcessChannelMode(QProcess::MergedChannels);
        } else {
            if (!(flags & SuppressStdErrInLogWindow)) {
                process.setStdErrBufferedSignalsEnabled(true);
                connect(&process, SIGNAL(stdErrBuffered(QString,bool)), outputWindow, SLOT(append(QString)));
            }
        }

        // connect stdout to the output window if desired
        if (flags & ShowStdOutInLogWindow) {
            process.setStdOutBufferedSignalsEnabled(true);
            connect(&process, SIGNAL(stdOutBuffered(QString,bool)), outputWindow, SLOT(append(QString)));
        }

        process.setTimeOutMessageBoxEnabled(true);

        // Run!
        response = process.run(binary, arguments);
    }

    // Success/Fail message in appropriate window?
    if (response.result == Utils::SynchronousProcessResponse::Finished) {
        if (flags & ShowSuccessMessage)
            outputWindow->append(response.exitMessage(binary, timeOutMS));
    } else {
        if (!(flags & SuppressFailMessageInLogWindow))
            outputWindow->appendError(response.exitMessage(binary, timeOutMS));
    }

    return response;
}

bool VCSBasePlugin::runFullySynchronous(const QString &workingDirectory,
                                        const QString &binary,
                                        const QStringList &arguments,
                                        const QProcessEnvironment &env,
                                        QByteArray* outputText,
                                        QByteArray* errorText,
                                        int timeoutMS,
                                        bool logCommandToWindow)
{
    VCSBase::VCSBaseOutputWindow *outputWindow = VCSBase::VCSBaseOutputWindow::instance();

    if (logCommandToWindow)
        outputWindow->appendCommand(workingDirectory, binary, arguments);

    QProcess process;
    process.setWorkingDirectory(workingDirectory);
    process.setProcessEnvironment(env);

    process.start(binary, arguments);
    process.closeWriteChannel();
    if (!process.waitForStarted()) {
        if (errorText) {
            const QString msg = QString::fromLatin1("Unable to execute '%1': %2:")
                                .arg(binary, process.errorString());
            *errorText = msg.toLocal8Bit();
        }
        return false;
    }

    if (!Utils::SynchronousProcess::readDataFromProcess(process, timeoutMS,
                                                        outputText, errorText, true)) {
        errorText->append(tr("Error: Executable timed out after %1s.").arg(timeoutMS / 1000).toLocal8Bit());
        Utils::SynchronousProcess::stopProcess(process);
        return false;
    }

    return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}

bool VCSBasePlugin::runPatch(const QByteArray &input, const QString &workingDirectory,
                             int strip, bool reverse)
{
    VCSBaseOutputWindow *ow = VCSBaseOutputWindow::instance();
    const QString patch = Internal::VCSPlugin::instance()->settings().patchCommand;
    if (patch.isEmpty()) {
        ow->appendError(tr("There is no patch-command configured in the common 'Version Control' settings."));
        return false;
    }

    QProcess patchProcess;
    if (!workingDirectory.isEmpty())
        patchProcess.setWorkingDirectory(workingDirectory);
    QStringList args(QLatin1String("-p") + QString::number(strip));
    if (reverse)
        args << QLatin1String("-R");
    ow->appendCommand(QString(), patch, args);
    patchProcess.start(patch, args);
    if (!patchProcess.waitForStarted()) {
        ow->appendError(tr("Unable to launch '%1': %2").arg(patch, patchProcess.errorString()));
        return false;
    }
    patchProcess.write(input);
    patchProcess.closeWriteChannel();
    QByteArray stdOut;
    QByteArray stdErr;
    if (!Utils::SynchronousProcess::readDataFromProcess(patchProcess, 30000, &stdOut, &stdErr, true)) {
        Utils::SynchronousProcess::stopProcess(patchProcess);
        ow->appendError(tr("A timeout occurred running '%1'").arg(patch));
        return false;

    }
    if (!stdOut.isEmpty())
        ow->append(QString::fromLocal8Bit(stdOut));
    if (!stdErr.isEmpty())
        ow->append(QString::fromLocal8Bit(stdErr));

    if (patchProcess.exitStatus() != QProcess::NormalExit) {
        ow->appendError(tr("'%1' crashed.").arg(patch));
        return false;
    }
    if (patchProcess.exitCode() != 0) {
        ow->appendError(tr("'%1' failed (exit code %2).").arg(patch).arg(patchProcess.exitCode()));
        return false;
    }
    return true;
}
} // namespace VCSBase

#include "vcsbaseplugin.moc"
