Skip to content
Snippets Groups Projects
gitclient.cpp 140 KiB
Newer Older
bool GitClient::synchronousForEachRefCmd(const QString &workingDirectory, QStringList args,
                                      QString *output, QString *errorMessage)
{
    args.push_front(QLatin1String("for-each-ref"));
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
    *output = commandOutputFromLocal8Bit(outputText);
    if (!rc) {
        QString error = msgCannotRun(QLatin1String("git for-each-ref"), workingDirectory,
                                     commandOutputFromLocal8Bit(errorText));
        if (errorMessage)
            *errorMessage = error;
        else
            outputWindow()->appendError(error);
bool GitClient::synchronousRemoteCmd(const QString &workingDirectory, QStringList remoteArgs,
                                     QString *output, QString *errorMessage)
{
    remoteArgs.push_front(QLatin1String("remote"));
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, remoteArgs, &outputText, &errorText);
    if (!rc) {
        *errorMessage = msgCannotRun(QLatin1String("git remote"), workingDirectory, commandOutputFromLocal8Bit(errorText));
        return false;
    }
    *output = commandOutputFromLocal8Bit(outputText);
    return true;
}

QMap<QString,QString> GitClient::synchronousRemotesList(const QString &workingDirectory,
                                                        QString *errorMessage)
{
    QMap<QString,QString> result;
    QString output;
    QString error;
    QStringList args(QLatin1String("-v"));
    if (!synchronousRemoteCmd(workingDirectory, args, &output, &error)) {
        if (errorMessage)
            *errorMessage = error;
        else
            outputWindow()->appendError(error);
        return result;
    }
    QStringList remotes = output.split(QLatin1String("\n"));

    foreach (const QString &remote, remotes) {
        if (!remote.endsWith(QLatin1String(" (push)")))
        int tabIndex = remote.indexOf(QLatin1Char('\t'));
        if (tabIndex == -1)
        QString url = remote.mid(tabIndex + 1, remote.length() - tabIndex - 8);
        result.insert(remote.left(tabIndex), url);
QStringList GitClient::synchronousSubmoduleStatus(const QString &workingDirectory,
                                                  QString *errorMessage)
{
    QByteArray outputTextData;
    QByteArray errorText;
    QStringList arguments;

    // get submodule status
    arguments << QLatin1String("submodule") << QLatin1String("status");
    if (!fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) {
        QString error = tr("Cannot retrieve submodule status of \"%1\": %2")
                .arg(QDir::toNativeSeparators(workingDirectory),
                     commandOutputFromLocal8Bit(errorText));

        if (errorMessage)
            *errorMessage = error;
        else
            outputWindow()->appendError(error);

        return QStringList();
    }
    return commandOutputLinesFromLocal8Bit(outputTextData);
}

SubmoduleDataMap GitClient::submoduleList(const QString &workingDirectory)
    SubmoduleDataMap result;
    QString gitmodulesFileName = workingDirectory + QLatin1String("/.gitmodules");
    if (!QFile::exists(gitmodulesFileName))
    static QMap<QString, SubmoduleDataMap> cachedSubmoduleData;

    if (cachedSubmoduleData.contains(workingDirectory))
        return cachedSubmoduleData.value(workingDirectory);
    QStringList args(QLatin1String("-l"));

    QStringList allConfigs = readConfig(workingDirectory, args).split(QLatin1Char('\n'));
    const QString submoduleLineStart = QLatin1String("submodule.");
    foreach (const QString &configLine, allConfigs) {
        if (!configLine.startsWith(submoduleLineStart))
            continue;

        int nameStart = submoduleLineStart.size();
        int nameEnd   = configLine.indexOf(QLatin1Char('.'), nameStart);

        QString submoduleName = configLine.mid(nameStart, nameEnd - nameStart);

        SubmoduleData submoduleData;
        if (result.contains(submoduleName))
            submoduleData = result[submoduleName];
        if (configLine.mid(nameEnd, 5) == QLatin1String(".url="))
            submoduleData.url = configLine.mid(nameEnd + 5);
        else if (configLine.mid(nameEnd, 8) == QLatin1String(".ignore="))
            submoduleData.ignore = configLine.mid(nameEnd + 8);
        else
            continue;

        result.insert(submoduleName, submoduleData);
    }

    // if config found submodules
    if (!result.isEmpty()) {
        QSettings gitmodulesFile(gitmodulesFileName, QSettings::IniFormat);

        foreach (const QString &submoduleName, result.keys()) {
            gitmodulesFile.beginGroup(QLatin1String("submodule \"") +
                                      submoduleName + QLatin1Char('"'));
            result[submoduleName].dir = gitmodulesFile.value(QLatin1String("path")).toString();
            QString ignore = gitmodulesFile.value(QLatin1String("ignore")).toString();
            if (!ignore.isEmpty() && result[submoduleName].ignore.isEmpty())
                result[submoduleName].ignore = ignore;
            gitmodulesFile.endGroup();
        }
    cachedSubmoduleData.insert(workingDirectory, result);
bool GitClient::synchronousShow(const QString &workingDirectory, const QString &id,
                                 QString *output, QString *errorMessage)
{
    if (!canShow(id)) {
        *errorMessage = msgCannotShow(id);
        return false;
    }
    QStringList args(QLatin1String("show"));
    args << QLatin1String(decorateOption) << QLatin1String(noColorOption) << id;
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
        *errorMessage = msgCannotRun(QLatin1String("git show"), workingDirectory, commandOutputFromLocal8Bit(errorText));
    *output = commandOutputFromLocal8Bit(outputText);
// Retrieve list of files to be cleaned
Orgad Shaneh's avatar
Orgad Shaneh committed
bool GitClient::cleanList(const QString &workingDirectory, const QString &flag, QStringList *files, QString *errorMessage)
Orgad Shaneh's avatar
Orgad Shaneh committed
    args << QLatin1String("clean") << QLatin1String("--dry-run") << flag;
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
        *errorMessage = msgCannotRun(QLatin1String("git clean"), workingDirectory, commandOutputFromLocal8Bit(errorText));
        return false;
    }
    // Filter files that git would remove
    const QString prefix = QLatin1String("Would remove ");
    foreach (const QString &line, commandOutputLinesFromLocal8Bit(outputText))
        if (line.startsWith(prefix))
            files->push_back(line.mid(prefix.size()));
    return true;
}

Orgad Shaneh's avatar
Orgad Shaneh committed
bool GitClient::synchronousCleanList(const QString &workingDirectory, QStringList *files,
                                     QStringList *ignoredFiles, QString *errorMessage)
{
    bool res = cleanList(workingDirectory, QLatin1String("-df"), files, errorMessage);
    res &= cleanList(workingDirectory, QLatin1String("-dXf"), ignoredFiles, errorMessage);

    SubmoduleDataMap submodules = submoduleList(workingDirectory);
    foreach (const SubmoduleData &submodule, submodules) {
        if (submodule.ignore != QLatin1String("all")
                && submodule.ignore != QLatin1String("dirty")) {
            res &= synchronousCleanList(workingDirectory + QLatin1Char('/') + submodule.dir,
                                        files, ignoredFiles, errorMessage);
        }
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
    return res;
}

bool GitClient::synchronousApplyPatch(const QString &workingDirectory,
                                      const QString &file, QString *errorMessage)
{
    QStringList args;
    args << QLatin1String("apply") << QLatin1String("--whitespace=fix") << file;
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
    if (rc) {
        if (!errorText.isEmpty())
Tobias Hunger's avatar
Tobias Hunger committed
            *errorMessage = tr("There were warnings while applying \"%1\" to \"%2\":\n%3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
Tobias Hunger's avatar
Tobias Hunger committed
        *errorMessage = tr("Cannot apply patch \"%1\" to \"%2\": %3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
// Factory function to create an asynchronous command
hjk's avatar
hjk committed
VcsBase::Command *GitClient::createCommand(const QString &workingDirectory,
                                           VcsBase::VcsBaseEditorWidget* editor,
Tobias Hunger's avatar
Tobias Hunger committed
                                           bool useOutputToWindow,
                                           int editorLineNumber)
con's avatar
con committed
{
hjk's avatar
hjk committed
    VcsBase::Command *command = new VcsBase::Command(gitBinaryPath(), workingDirectory, processEnvironment());
    command->setCodec(getSourceCodec(currentDocumentPath()));
Tobias Hunger's avatar
Tobias Hunger committed
    command->setCookie(QVariant(editorLineNumber));
        connect(command, SIGNAL(finished(bool,int,QVariant)), editor, SLOT(commandFinishedGotoLine(bool,int,QVariant)));
    if (useOutputToWindow) {
        if (editor) // assume that the commands output is the important thing
            connect(command, SIGNAL(output(QString)), this, SLOT(appendOutputSilently(QString)));
            connect(command, SIGNAL(output(QString)), this, SLOT(appendOutput(QString)));
    } else if (editor) {
        connect(command, SIGNAL(output(QString)), editor, SLOT(setPlainTextFiltered(QString)));
con's avatar
con committed
    }

    connect(command, SIGNAL(errorText(QString)), outputWindow(), SLOT(appendError(QString)));
con's avatar
con committed

hjk's avatar
hjk committed
VcsBase::Command *GitClient::executeGit(const QString &workingDirectory,
Tobias Hunger's avatar
Tobias Hunger committed
                                        const QStringList &arguments,
hjk's avatar
hjk committed
                                        VcsBase::VcsBaseEditorWidget* editor,
Tobias Hunger's avatar
Tobias Hunger committed
                                        bool useOutputToWindow,
                                        bool expectChanges,
    outputWindow()->appendCommand(workingDirectory, settings()->stringValue(GitSettings::binaryPathKey), arguments);
hjk's avatar
hjk committed
    VcsBase::Command *command = createCommand(workingDirectory, editor, useOutputToWindow, editorLineNumber);
    command->addJob(arguments, settings()->intValue(GitSettings::timeoutKey));
    command->setUnixTerminalDisabled(false);
    command->setExpectChanges(expectChanges);
con's avatar
con committed
}

QProcessEnvironment GitClient::processEnvironment() const
    QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
    QString gitPath = settings()->stringValue(GitSettings::pathKey);
    if (!gitPath.isEmpty()) {
        gitPath += Utils::HostOsInfo::pathListSeparator();
        gitPath += environment.value(QLatin1String("PATH"));
        environment.insert(QLatin1String("PATH"), gitPath);
    }
    if (Utils::HostOsInfo::isWindowsHost()
            && settings()->boolValue(GitSettings::winSetHomeEnvironmentKey)) {
        environment.insert(QLatin1String("HOME"), QDir::toNativeSeparators(QDir::homePath()));
    environment.insert(QLatin1String("GIT_EDITOR"), m_disableEditor ? QLatin1String("true") : m_gitQtcEditor);
    // Set up SSH and C locale (required by git using perl).
    VcsBasePlugin::setProcessEnvironment(&environment, false);
bool GitClient::beginStashScope(const QString &workingDirectory, const QString &command, StashFlag flag)
    const QString repoDirectory = findRepositoryForDirectory(workingDirectory);
    QTC_ASSERT(!repoDirectory.isEmpty(), return false);
    StashInfo &stashInfo = m_stashInfo[repoDirectory];
    return stashInfo.init(repoDirectory, command, flag);
}

GitClient::StashInfo &GitClient::stashInfo(const QString &workingDirectory)
{
    const QString repoDirectory = findRepositoryForDirectory(workingDirectory);
    QTC_CHECK(m_stashInfo.contains(repoDirectory));
    return m_stashInfo[repoDirectory];
}

void GitClient::endStashScope(const QString &workingDirectory)
{
    const QString repoDirectory = findRepositoryForDirectory(workingDirectory);
    QTC_ASSERT(m_stashInfo.contains(repoDirectory), return);
    m_stashInfo[repoDirectory].end();
bool GitClient::isValidRevision(const QString &revision) const
{
    if (revision.length() < 1)
        return false;
    for (int i = 0; i < revision.length(); ++i)
        if (revision.at(i) != QLatin1Char('0'))
            return true;
    return false;
}

// Synchronous git execution using Utils::SynchronousProcess, with
// log windows updating.
Tobias Hunger's avatar
Tobias Hunger committed
Utils::SynchronousProcessResponse GitClient::synchronousGit(const QString &workingDirectory,
                                                            const QStringList &gitArguments,
                                                            unsigned flags,
    return VcsBasePlugin::runVcs(workingDirectory, gitBinaryPath(), gitArguments,
                                 settings()->intValue(GitSettings::timeoutKey) * 1000,
                                 processEnvironment(), VcsBase::VcsBasePlugin::sshPrompt(),
}

bool GitClient::fullySynchronousGit(const QString &workingDirectory,
                                    const QStringList &gitArguments,
                                    QByteArray* outputText,
                                    QByteArray* errorText,
    return VcsBasePlugin::runFullySynchronous(workingDirectory, gitBinaryPath(), gitArguments,
                                              processEnvironment(), outputText, errorText,
                                              settings()->intValue(GitSettings::timeoutKey) * 1000,
con's avatar
con committed
}

void GitClient::updateSubmodulesIfNeeded(const QString &workingDirectory, bool prompt)
    if (!m_updatedSubmodules.isEmpty() || submoduleList(workingDirectory).isEmpty())
        return;

    QStringList submoduleStatus = synchronousSubmoduleStatus(workingDirectory);
    if (submoduleStatus.isEmpty())
        return;

    bool updateNeeded = false;
    foreach (const QString &status, submoduleStatus) {
        if (status.startsWith(QLatin1Char('+'))) {
            updateNeeded = true;
            break;
        }
    }
    if (!updateNeeded)
        return;

    if (prompt && QMessageBox::question(Core::ICore::mainWindow(), tr("Submodules Found"),
            tr("Would you like to update submodules?"),
            QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
        return;
    }

    foreach (const QString &statusLine, submoduleStatus) {
        // stash only for lines starting with +
        // because only they would be updated
        if (!statusLine.startsWith(QLatin1Char('+')))
            continue;

        // get submodule name
        const int nameStart  = statusLine.indexOf(QLatin1Char(' '), 2) + 1;
        const int nameLength = statusLine.indexOf(QLatin1Char(' '), nameStart) - nameStart;
        const QString submoduleDir = workingDirectory + QLatin1Char('/')
                + statusLine.mid(nameStart, nameLength);

        if (beginStashScope(submoduleDir, QLatin1String("SubmoduleUpdate"))) {
            m_updatedSubmodules.append(submoduleDir);
        } else {
            finishSubmoduleUpdate();
            return;
        }
    }

    QStringList arguments;
    arguments << QLatin1String("submodule") << QLatin1String("update");

    VcsBase::Command *cmd = executeGit(workingDirectory, arguments, 0, true, true);
    connect(cmd, SIGNAL(finished(bool,int,QVariant)), this, SLOT(finishSubmoduleUpdate()));
void GitClient::finishSubmoduleUpdate()
    foreach (const QString &submoduleDir, m_updatedSubmodules)
        endStashScope(submoduleDir);
    m_updatedSubmodules.clear();
void GitClient::fetchFinished(const QVariant &cookie)
{
    GitPlugin::instance()->updateBranches(cookie.toString());
}

// Trim a git status file spec: "modified:    foo .cpp" -> "modified: foo .cpp"
static inline QString trimFileSpecification(QString fileSpec)
{
    const int colonIndex = fileSpec.indexOf(QLatin1Char(':'));
    if (colonIndex != -1) {
        // Collapse the sequence of spaces
        const int filePos = colonIndex + 2;
        int nonBlankPos = filePos;
        for ( ; fileSpec.at(nonBlankPos).isSpace(); nonBlankPos++) ;
        if (nonBlankPos > filePos)
            fileSpec.remove(filePos, nonBlankPos - filePos);
    }
    return fileSpec;
}

GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory, StatusMode mode,
                                             QString *output, QString *errorMessage)
{
    // Run 'status'. Note that git returns exitcode 1 if there are no added files.
    QByteArray outputText;
    QByteArray errorText;
    QStringList statusArgs = statusArguments();
    if (mode & NoUntracked)
        statusArgs << QLatin1String("--untracked-files=no");
        statusArgs << QLatin1String("--untracked-files=all");
    if (mode & NoSubmodules)
        statusArgs << QLatin1String("--ignore-submodules=all");
    statusArgs << QLatin1String("-s") << QLatin1String("-b");

Orgad Shaneh's avatar
Orgad Shaneh committed
    const bool statusRc = fullySynchronousGit(workingDirectory, statusArgs,
                                              &outputText, &errorText, false);
        *output = commandOutputFromLocal8Bit(outputText);

    static const char * NO_BRANCH = "## HEAD (no branch)\n";

    const bool branchKnown = !outputText.startsWith(NO_BRANCH);
    // Is it something really fatal?
    if (!statusRc && !branchKnown) {
            const QString error = commandOutputFromLocal8Bit(errorText);
Tobias Hunger's avatar
Tobias Hunger committed
            *errorMessage = tr("Cannot obtain status: %1").arg(error);
Friedemann Kleint's avatar
Friedemann Kleint committed
    // Unchanged (output text depending on whether -u was passed)
    QList<QByteArray> lines = outputText.split('\n');
    foreach (const QByteArray &line, lines)
        if (!line.isEmpty() && !line.startsWith('#'))
            return StatusChanged;
    return StatusUnchanged;
GitClient::CommandInProgress GitClient::checkCommandInProgressInGitDir(const QString &gitDir)
    if (QFile::exists(gitDir + QLatin1String("/MERGE_HEAD")))
        return Merge;
    else if (QFile::exists(gitDir + QLatin1String("/rebase-apply/rebasing")))
        return Rebase;
    else if (QFile::exists(gitDir + QLatin1String("/rebase-merge")))
        return RebaseMerge;
    else if (QFile::exists(gitDir + QLatin1String("/REVERT_HEAD")))
        return Revert;
    else if (QFile::exists(gitDir + QLatin1String("/CHERRY_PICK_HEAD")))
        return CherryPick;
    else
        return NoCommand;
}

GitClient::CommandInProgress GitClient::checkCommandInProgress(const QString &workingDirectory)
{
    return checkCommandInProgressInGitDir(findGitDirForRepository(workingDirectory));
}

void GitClient::continueCommandIfNeeded(const QString &workingDirectory)
{
    CommandInProgress command = checkCommandInProgress(workingDirectory);
    switch (command) {
    case Merge:
        continuePreviousGitCommand(workingDirectory, tr("Continue Merge"),
                                   tr("Merge is in progress. What do you want to do?"),
                                   tr("Continue"), QLatin1String("merge"));
        break;
    case Rebase:
    case RebaseMerge:
Orgad Shaneh's avatar
Orgad Shaneh committed
        continuePreviousGitCommand(workingDirectory, tr("Continue Rebase"),
                                   tr("Rebase is in progress. What do you want to do?"),
                                   tr("Continue"), QLatin1String("rebase"),
                                   command != RebaseMerge);
        break;
    case Revert:
        continuePreviousGitCommand(workingDirectory, tr("Continue Revert"),
                tr("You need to commit changes to finish revert.\nCommit now?"),
                tr("Commit"), QLatin1String("revert"));
        break;
    case CherryPick:
        continuePreviousGitCommand(workingDirectory, tr("Continue Cherry-Picking"),
                tr("You need to commit changes to finish cherry-picking.\nCommit now?"),
                tr("Commit"), QLatin1String("cherry-pick"));
        break;
    default:
        break;
    }
}

void GitClient::continuePreviousGitCommand(const QString &workingDirectory,
                                           const QString &msgBoxTitle, QString msgBoxText,
Orgad Shaneh's avatar
Orgad Shaneh committed
                                           const QString &buttonName, const QString &gitCommand,
                                           bool requireChanges)
{
    bool isRebase = gitCommand == QLatin1String("rebase");
Orgad Shaneh's avatar
Orgad Shaneh committed
    bool hasChanges;
    if (!requireChanges) {
        hasChanges = true;
    } else {
        hasChanges = gitStatus(workingDirectory, StatusMode(NoUntracked | NoSubmodules))
            == GitClient::StatusChanged;
    if (!hasChanges)
        msgBoxText.prepend(tr("No changes found. "));
Orgad Shaneh's avatar
Orgad Shaneh committed
    QMessageBox msgBox(QMessageBox::Question, msgBoxTitle, msgBoxText,
                       QMessageBox::NoButton, Core::ICore::mainWindow());
    if (hasChanges || isRebase)
        msgBox.addButton(hasChanges ? buttonName : tr("Skip"), QMessageBox::AcceptRole);
    msgBox.addButton(QMessageBox::Abort);
    msgBox.addButton(QMessageBox::Ignore);
    switch (msgBox.exec()) {
    case QMessageBox::Ignore:
        break;
    case QMessageBox::Abort:
        synchronousAbortCommand(workingDirectory, gitCommand);
        break;
    default: // Continue/Skip
        if (isRebase)
            rebase(workingDirectory, QLatin1String(hasChanges ? "--continue" : "--skip"));
        else
            GitPlugin::instance()->startCommit();
    }
}

// Quietly retrieve branch list of remote repository URL
Tobias Hunger's avatar
Tobias Hunger committed
//
// The branch HEAD is pointing to is always returned first.
QStringList GitClient::synchronousRepositoryBranches(const QString &repositoryURL)
{
    QStringList arguments(QLatin1String("ls-remote"));
    arguments << repositoryURL << QLatin1String(HEAD) << QLatin1String("refs/heads/*");
Orgad Shaneh's avatar
Orgad Shaneh committed
    const unsigned flags = VcsBasePlugin::SshPasswordPrompt
            | VcsBasePlugin::SuppressStdErrInLogWindow
            | VcsBasePlugin::SuppressFailMessageInLogWindow;
    const Utils::SynchronousProcessResponse resp = synchronousGit(QString(), arguments, flags);
    QStringList branches;
    branches << tr("<Detached HEAD>");
Tobias Hunger's avatar
Tobias Hunger committed
    QString headSha;
    // split "82bfad2f51d34e98b18982211c82220b8db049b<tab>refs/heads/master"
    foreach (const QString &line, resp.stdOut.split(QLatin1Char('\n'))) {
        if (line.endsWith(QLatin1String("\tHEAD"))) {
            QTC_CHECK(headSha.isNull());
            headSha = line.left(line.indexOf(QLatin1Char('\t')));
            continue;
        }

        const QString pattern = QLatin1String("\trefs/heads/");
        const int pos = line.lastIndexOf(pattern);
        if (pos != -1) {
            const QString branchName = line.mid(pos + pattern.count());
            if (!headFound && line.startsWith(headSha)) {
                branches[0] = branchName;
                branches.push_back(branchName);
Petar Perisin's avatar
Petar Perisin committed
void GitClient::launchGitK(const QString &workingDirectory, const QString &fileName)
    const QFileInfo binaryInfo(gitBinaryPath());
    QDir foundBinDir(binaryInfo.dir());
    const bool foundBinDirIsCmdDir = foundBinDir.dirName() == QLatin1String("cmd");
    QProcessEnvironment env = processEnvironment();
Petar Perisin's avatar
Petar Perisin committed
    if (tryLauchingGitK(env, workingDirectory, fileName, foundBinDir.path(), foundBinDirIsCmdDir))
        return;
    if (!foundBinDirIsCmdDir)
        return;
    foundBinDir.cdUp();
Petar Perisin's avatar
Petar Perisin committed
    tryLauchingGitK(env, workingDirectory, fileName, foundBinDir.path() + QLatin1String("/bin"), false);
void GitClient::launchRepositoryBrowser(const QString &workingDirectory)
{
    const QString repBrowserBinary = settings()->stringValue(GitSettings::repositoryBrowserCmd);
    if (!repBrowserBinary.isEmpty())
        QProcess::startDetached(repBrowserBinary, QStringList(workingDirectory), workingDirectory);
}

bool GitClient::tryLauchingGitK(const QProcessEnvironment &env,
                                const QString &workingDirectory,
Petar Perisin's avatar
Petar Perisin committed
                                const QString &fileName,
                                const QString &gitBinDirectory,
                                bool silent)
{
    QString binary = gitBinDirectory + QLatin1String("/gitk");
    if (Utils::HostOsInfo::isWindowsHost()) {
        // If git/bin is in path, use 'wish' shell to run. Otherwise (git/cmd), directly run gitk
        QString wish = gitBinDirectory + QLatin1String("/wish");
        if (QFileInfo(wish + QLatin1String(".exe")).exists()) {
            arguments << binary;
            binary = wish;
        }
hjk's avatar
hjk committed
    VcsBase::VcsBaseOutputWindow *outwin = VcsBase::VcsBaseOutputWindow::instance();
    const QString gitkOpts = settings()->stringValue(GitSettings::gitkOptionsKey);
    if (!gitkOpts.isEmpty())
        arguments.append(Utils::QtcProcess::splitArgs(gitkOpts));
Petar Perisin's avatar
Petar Perisin committed
    if (!fileName.isEmpty())
        arguments << QLatin1String("--") << fileName;
    outwin->appendCommand(workingDirectory, binary, arguments);
    // This should always use QProcess::startDetached (as not to kill
    // the child), but that does not have an environment parameter.
    bool success = false;
    if (!settings()->stringValue(GitSettings::pathKey).isEmpty()) {
        QProcess *process = new QProcess(this);
        process->setWorkingDirectory(workingDirectory);
        process->setProcessEnvironment(env);
        process->start(binary, arguments);
        success = process->waitForStarted();
Tobias Hunger's avatar
Tobias Hunger committed
        if (success)
            connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
Tobias Hunger's avatar
Tobias Hunger committed
        else
            delete process;
    } else {
        success = QProcess::startDetached(binary, arguments, workingDirectory);
    if (!success) {
        if (silent)
Friedemann Kleint's avatar
Friedemann Kleint committed
            outwin->appendSilently(msgCannotLaunch(binary));
Friedemann Kleint's avatar
Friedemann Kleint committed
            outwin->appendError(msgCannotLaunch(binary));
    }
    return success;
bool GitClient::launchGitGui(const QString &workingDirectory) {
    bool success;
    QString gitBinary = gitBinaryPath(&success);
    if (success)
        success = QProcess::startDetached(gitBinary, QStringList(QLatin1String("gui")),
                                          workingDirectory);

    if (!success)
Friedemann Kleint's avatar
Friedemann Kleint committed
        outputWindow()->appendError(msgCannotLaunch(QLatin1String("git gui")));
QString GitClient::gitBinaryPath(bool *ok, QString *errorMessage) const
{
    return settings()->gitBinaryPath(ok, errorMessage);
}

con's avatar
con committed
bool GitClient::getCommitData(const QString &workingDirectory,
                              QString *commitTemplate,
                              CommitData &commitData,
con's avatar
con committed
                              QString *errorMessage)
{
    commitData.clear();
con's avatar
con committed

    // Find repo
    const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
    if (repoDirectory.isEmpty()) {
        *errorMessage = msgRepositoryNotFound(workingDirectory);
con's avatar
con committed
        return false;
    }

    commitData.panelInfo.repository = repoDirectory;
con's avatar
con committed

    QString gitDir = findGitDirForRepository(repoDirectory);
    if (gitDir.isEmpty()) {
Tobias Hunger's avatar
Tobias Hunger committed
        *errorMessage = tr("The repository \"%1\" is not initialized.").arg(repoDirectory);
con's avatar
con committed
        return false;
    }

    // Run status. Note that it has exitcode 1 if there are no added files.
    if (commitData.commitType == FixupCommit) {
        QStringList arguments;
        arguments << QLatin1String("HEAD") << QLatin1String("--not")
                  << QLatin1String("--remotes") << QLatin1String("-n1");
        synchronousLog(repoDirectory, arguments, &output, errorMessage);
        if (output.isEmpty()) {
            *errorMessage = msgNoCommits(false);
            return false;
        }
    }
    const StatusResult status = gitStatus(repoDirectory, ShowAll, &output, errorMessage);
    switch (status) {
    case  StatusChanged:
        break;
    case StatusUnchanged:
        if (commitData.commitType == AmendCommit) // amend might be run just for the commit message
        *errorMessage = msgNoChangedFiles();
        return false;
    case StatusFailed:
        return false;
con's avatar
con committed
    }

    //    Output looks like:
    //    ## branch_name
    //    MM filename
    //     A new_unstaged_file
    //    R  old -> new
    //     D deleted_file
    //    ?? untracked_file
    if (status != StatusUnchanged) {
        if (!commitData.parseFilesFromStatus(output)) {
            *errorMessage = msgParseFilesFailed();
            return false;
        }
        // Filter out untracked files that are not part of the project
        QStringList untrackedFiles = commitData.filterFiles(UntrackedFile);
hjk's avatar
hjk committed
        VcsBase::VcsBaseSubmitEditor::filterUntrackedFilesOfProject(repoDirectory, &untrackedFiles);
        QList<CommitData::StateFilePair> filteredFiles;
        QList<CommitData::StateFilePair>::const_iterator it = commitData.files.constBegin();
        for ( ; it != commitData.files.constEnd(); ++it) {
            if (it->first == UntrackedFile && !untrackedFiles.contains(it->second))
                continue;
            filteredFiles.append(*it);
        }
        commitData.files = filteredFiles;
        if (commitData.files.isEmpty() && commitData.commitType != AmendCommit) {
            *errorMessage = msgNoChangedFiles();
            return false;
        }
con's avatar
con committed

    commitData.commitEncoding = readConfigValue(workingDirectory, QLatin1String("i18n.commitEncoding"));

    // Get the commit template or the last commit message
    switch (commitData.commitType) {
    case AmendCommit: {
        // Amend: get last commit data as "SHA1<tab>author<tab>email<tab>message".
        QStringList args(QLatin1String("log"));
Orgad Shaneh's avatar
Orgad Shaneh committed
        args << QLatin1String("--max-count=1") << QLatin1String("--pretty=format:%h\t%an\t%ae\t%B");
        QTextCodec *codec = QTextCodec::codecForName(commitData.commitEncoding.toLocal8Bit());
        const Utils::SynchronousProcessResponse sp = synchronousGit(repoDirectory, args, 0, codec);
        if (sp.result != Utils::SynchronousProcessResponse::Finished) {
Tobias Hunger's avatar
Tobias Hunger committed
            *errorMessage = tr("Cannot retrieve last commit data of repository \"%1\".").arg(repoDirectory);
        QStringList values = sp.stdOut.split(QLatin1Char('\t'));
        QTC_ASSERT(values.size() >= 4, return false);
        commitData.amendSHA1 = values.takeFirst();
        commitData.panelData.author = values.takeFirst();
        commitData.panelData.email = values.takeFirst();
        *commitTemplate = values.join(QLatin1String("\t"));
        break;
    }
    case SimpleCommit: {
        commitData.panelData.author = readConfigValue(workingDirectory, QLatin1String("user.name"));
        commitData.panelData.email = readConfigValue(workingDirectory, QLatin1String("user.email"));
        // Commit: Get the commit template
        QDir gitDirectory(gitDir);
        QString templateFilename = gitDirectory.absoluteFilePath(QLatin1String("MERGE_MSG"));
        if (!QFile::exists(templateFilename))
            templateFilename = gitDirectory.absoluteFilePath(QLatin1String("SQUASH_MSG"));
        if (!QFile::exists(templateFilename)) {
            Utils::FileName templateName = Utils::FileName::fromUserInput(
                        readConfigValue(workingDirectory, QLatin1String("commit.template")));
            templateFilename = templateName.toString();
        }
        if (!templateFilename.isEmpty()) {
            // Make relative to repository
            const QFileInfo templateFileInfo(templateFilename);
            if (templateFileInfo.isRelative())
                templateFilename = repoDirectory + QLatin1Char('/') + templateFilename;
            Utils::FileReader reader;
            if (!reader.fetch(templateFilename, QIODevice::Text, errorMessage))
                return false;
            *commitTemplate = QString::fromLocal8Bit(reader.data());
con's avatar
con committed
        }
    case FixupCommit:
        break;
con's avatar
con committed
    }
    return true;
}

// Log message for commits/amended commits to go to output window
static inline QString msgCommitted(const QString &amendSHA1, int fileCount)
{
    if (amendSHA1.isEmpty())
        return GitClient::tr("Committed %n file(s).\n", 0, fileCount);
    if (fileCount)
Tobias Hunger's avatar
Tobias Hunger committed
        return GitClient::tr("Amended \"%1\" (%n file(s)).\n", 0, fileCount).arg(amendSHA1);
    return GitClient::tr("Amended \"%1\".").arg(amendSHA1);
bool GitClient::addAndCommit(const QString &repositoryDirectory,
con's avatar
con committed
                             const GitSubmitEditorPanelData &data,
                             CommitType commitType,
                             const QString &amendSHA1,
con's avatar
con committed
                             const QString &messageFile,
hjk's avatar
hjk committed
                             VcsBase::SubmitFileModel *model)
con's avatar
con committed
{
    const QString renameSeparator = QLatin1String(" -> ");
    QStringList filesToAdd;
    QStringList filesToRemove;
    QStringList filesToReset;

    int commitCount = 0;

    for (int i = 0; i < model->rowCount(); ++i) {
        const FileStates state = static_cast<FileStates>(model->extraData(i).toInt());
        QString file = model->file(i);
        const bool checked = model->checked(i);

        if (checked)
            ++commitCount;

        if (state == UntrackedFile && checked)
            filesToAdd.append(file);

        if ((state & StagedFile) && !checked) {
            if (state & (ModifiedFile | AddedFile | DeletedFile)) {
                filesToReset.append(file);
            } else if (state & (RenamedFile | CopiedFile)) {
                const QString newFile = file.mid(file.indexOf(renameSeparator) + renameSeparator.count());
                filesToReset.append(newFile);
            }
        } else if (state & UnmergedFile && checked) {
            QTC_ASSERT(false, continue); // There should not be unmerged files when commiting!
        if (state == ModifiedFile && checked) {
            filesToReset.removeAll(file);
            filesToAdd.append(file);
        } else if (state == AddedFile && checked) {
            QTC_ASSERT(false, continue); // these should be untracked!
        } else if (state == DeletedFile && checked) {
            filesToReset.removeAll(file);
            filesToRemove.append(file);
        } else if (state == RenamedFile && checked) {
            QTC_ASSERT(false, continue); // git mv directly stages.
        } else if (state == CopiedFile && checked) {
            QTC_ASSERT(false, continue); // only is noticed after adding a new file to the index
        } else if (state == UnmergedFile && checked) {
            QTC_ASSERT(false, continue); // There should not be unmerged files when commiting!
    if (!filesToReset.isEmpty() && !synchronousReset(repositoryDirectory, filesToReset))
        return false;
    if (!filesToRemove.isEmpty() && !synchronousDelete(repositoryDirectory, true, filesToRemove))
        return false;

    if (!filesToAdd.isEmpty() && !synchronousAdd(repositoryDirectory, false, filesToAdd))
        return false;
con's avatar
con committed

    // Do the final commit
    QStringList args;
    args << QLatin1String("commit");
    if (commitType == FixupCommit) {
        args << QLatin1String("--fixup") << amendSHA1;
    } else {
        args << QLatin1String("-F") << QDir::toNativeSeparators(messageFile);
        if (commitType == AmendCommit)
            args << QLatin1String("--amend");
        const QString &authorString =  data.authorString();
        if (!authorString.isEmpty())
             args << QLatin1String("--author") << authorString;
        if (data.bypassHooks)
            args << QLatin1String("--no-verify");
    }
con's avatar
con committed

    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(repositoryDirectory, args, &outputText, &errorText);
    const QString stdErr = commandOutputFromLocal8Bit(errorText);
    if (rc) {
        outputWindow()->append(msgCommitted(amendSHA1, commitCount));
        outputWindow()->appendError(stdErr);
    } else {
        outputWindow()->appendError(tr("Cannot commit %n file(s): %1\n", 0, commitCount).arg(stdErr));
    }
con's avatar
con committed
    return rc;
}

/* Revert: This function can be called with a file list (to revert single
 * files)  or a single directory (revert all). Qt Creator currently has only
 * 'revert single' in its VCS menus, but the code is prepared to deal with
 * reverting a directory pending a sophisticated selection dialog in the
hjk's avatar
hjk committed
 * VcsBase plugin. */
GitClient::RevertResult GitClient::revertI(QStringList files,
                                           bool *ptrToIsDirectory,
                                           QString *errorMessage,
                                           bool revertStaging)
{
    if (files.empty())
        return RevertCanceled;

    // Figure out the working directory
    const QFileInfo firstFile(files.front());
    const bool isDirectory = firstFile.isDir();
    if (ptrToIsDirectory)
        *ptrToIsDirectory = isDirectory;
    const QString workingDirectory = isDirectory ? firstFile.absoluteFilePath() : firstFile.absolutePath();

    const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
    if (repoDirectory.isEmpty()) {
        *errorMessage = msgRepositoryNotFound(workingDirectory);
        return RevertFailed;
    }

    // Check for changes
    QString output;
    switch (gitStatus(repoDirectory, StatusMode(NoUntracked | NoSubmodules), &output, errorMessage)) {
    case StatusChanged:
        break;
    case StatusUnchanged:
        return RevertUnchanged;
    case StatusFailed:
        return RevertFailed;
    }
    if (!data.parseFilesFromStatus(output)) {
        *errorMessage = msgParseFilesFailed();
        return RevertFailed;
    }

    // If we are looking at files, make them relative to the repository
    // directory to match them in the status output list.
    if (!isDirectory) {
        const QDir repoDir(repoDirectory);
        const QStringList::iterator cend = files.end();
        for (QStringList::iterator it = files.begin(); it != cend; ++it)
            *it = repoDir.relativeFilePath(*it);
    }

    // From the status output, determine all modified [un]staged files.
    const QStringList allStagedFiles = data.filterFiles(StagedFile | ModifiedFile);
    const QStringList allUnstagedFiles = data.filterFiles(ModifiedFile);
    // Unless a directory was passed, filter all modified files for the
    // argument file list.
    QStringList stagedFiles = allStagedFiles;
    QStringList unstagedFiles = allUnstagedFiles;
    if (!isDirectory) {
        const QSet<QString> filesSet = files.toSet();
        stagedFiles = allStagedFiles.toSet().intersect(filesSet).toList();
        unstagedFiles = allUnstagedFiles.toSet().intersect(filesSet).toList();
    }
    if ((!revertStaging || stagedFiles.empty()) && unstagedFiles.empty())
        return RevertUnchanged;

    // Ask to revert (to do: Handle lists with a selection dialog)
    const QMessageBox::StandardButton answer
hjk's avatar
hjk committed
        = QMessageBox::question(Core::ICore::mainWindow(),
                                tr("Revert"),
                                tr("The file has been changed. Do you want to revert it?"),
Orgad Shaneh's avatar
Orgad Shaneh committed
                                QMessageBox::Yes | QMessageBox::No,
                                QMessageBox::No);
    if (answer == QMessageBox::No)
        return RevertCanceled;

    // Unstage the staged files
    if (revertStaging && !stagedFiles.empty() && !synchronousReset(repoDirectory, stagedFiles, errorMessage))
    QStringList filesToRevert = unstagedFiles;
    if (revertStaging)
        filesToRevert += stagedFiles;
    if (!synchronousCheckoutFiles(repoDirectory, filesToRevert, QString(), errorMessage, revertStaging))
void GitClient::revert(const QStringList &files, bool revertStaging)
    switch (revertI(files, &isDirectory, &errorMessage, revertStaging)) {