Skip to content
Snippets Groups Projects
gitclient.cpp 151 KiB
Newer Older
        // 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()->appendMessage(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)) {
        GitPlugin::instance()->gitVersionControl()->emitFilesChanged(files);
    case RevertCanceled:
        break;
    case RevertUnchanged: {
        const QString msg = (isDirectory || files.size() > 1) ? msgNoChangedFiles() : tr("The file is not modified.");
        outputWindow()->appendWarning(msg);
        outputWindow()->appendError(errorMessage);
void GitClient::fetch(const QString &workingDirectory, const QString &remote)
{
    QStringList arguments(QLatin1String("fetch"));
    arguments << (remote.isEmpty() ? QLatin1String("--all") : remote);
    VcsBase::Command *command = executeGit(workingDirectory, arguments, 0, true);
    command->setCookie(workingDirectory);
    connect(command, SIGNAL(success(QVariant)), this, SLOT(fetchFinished(QVariant)));
bool GitClient::executeAndHandleConflicts(const QString &workingDirectory,
                                          const QStringList &arguments,
                                          const QString &abortCommand)
    // Disable UNIX terminals to suppress SSH prompting.
Orgad Shaneh's avatar
Orgad Shaneh committed
    const unsigned flags = VcsBasePlugin::SshPasswordPrompt
            | VcsBasePlugin::ShowStdOutInLogWindow
            | VcsBasePlugin::ExpectRepoChanges;
    const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, arguments, flags);
    ConflictHandler conflictHandler(0, workingDirectory, abortCommand);
    // Notify about changed files or abort the rebase.
    const bool ok = resp.result == Utils::SynchronousProcessResponse::Finished;
        conflictHandler.readStdOut(resp.stdOut);
        conflictHandler.readStdErr(resp.stdErr);
bool GitClient::synchronousPull(const QString &workingDirectory, bool rebase)
{
Orgad Shaneh's avatar
Orgad Shaneh committed
    QStringList arguments(QLatin1String("pull"));
        arguments << QLatin1String("--rebase");
        abortCommand = QLatin1String("rebase");
    } else {
        abortCommand = QLatin1String("merge");
    }

    bool ok = executeAndHandleConflicts(workingDirectory, arguments, abortCommand);

    if (ok)
        updateSubmodulesIfNeeded(workingDirectory, true);

    return ok;
void GitClient::synchronousAbortCommand(const QString &workingDir, const QString &abortCommand)
{
    // Abort to clean if something goes wrong
    if (abortCommand.isEmpty()) {
        // no abort command - checkout index to clean working copy.
        synchronousCheckoutFiles(findRepositoryForDirectory(workingDir), QStringList(), QString(), 0, false);
        return;
    }
    VcsBase::VcsBaseOutputWindow *outwin = VcsBase::VcsBaseOutputWindow::instance();
    QStringList arguments;
    arguments << abortCommand << QLatin1String("--abort");
    QByteArray stdOut;
    QByteArray stdErr;
    const bool rc = fullySynchronousGit(workingDir, arguments, &stdOut, &stdErr,
                                        VcsBasePlugin::ExpectRepoChanges);
    outwin->append(commandOutputFromLocal8Bit(stdOut));
    if (!rc)
        outwin->appendError(commandOutputFromLocal8Bit(stdErr));
}

QString GitClient::synchronousTrackingBranch(const QString &workingDirectory, const QString &branch)
{
    QString remote;
    QString localBranch = branch.isEmpty() ? synchronousCurrentLocalBranch(workingDirectory) : branch;
    if (localBranch.isEmpty())
        return QString();
    localBranch.prepend(QLatin1String("branch."));
    remote = readConfigValue(workingDirectory, localBranch + QLatin1String(".remote"));
    if (remote.isEmpty())
        return QString();
    const QString rBranch = readConfigValue(workingDirectory, localBranch + QLatin1String(".merge"))
            .replace(QLatin1String("refs/heads/"), QString());
    if (rBranch.isEmpty())
        return QString();
    return remote + QLatin1Char('/') + rBranch;
}

bool GitClient::synchronousSetTrackingBranch(const QString &workingDirectory,
                                             const QString &branch, const QString &tracking)
{
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
    arguments << QLatin1String("branch");
    if (gitVersion() >= 0x010800)
        arguments << (QLatin1String("--set-upstream-to=") + tracking) << branch;
    else
        arguments << QLatin1String("--set-upstream") << branch << tracking;
    const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
    if (!rc) {
        const QString errorMessage = tr("Cannot set tracking branch: %1")
                                     .arg(commandOutputFromLocal8Bit(errorText));
        outputWindow()->appendError(errorMessage);
    }
    return rc;
}

void GitClient::handleMergeConflicts(const QString &workingDir, const QString &commit,
                                     const QStringList &files, const QString &abortCommand)
    QString message;
    if (!commit.isEmpty())
Friedemann Kleint's avatar
Friedemann Kleint committed
        message = tr("Conflicts detected with commit %1.").arg(commit);
    else if (!files.isEmpty())
Friedemann Kleint's avatar
Friedemann Kleint committed
        message = tr("Conflicts detected with files:\n%1").arg(files.join(QLatin1String("\n")));
Friedemann Kleint's avatar
Friedemann Kleint committed
        message = tr("Conflicts detected.");
Orgad Shaneh's avatar
Orgad Shaneh committed
    QMessageBox mergeOrAbort(QMessageBox::Question, tr("Conflicts Detected"), message,
                             QMessageBox::NoButton, Core::ICore::mainWindow());
    QPushButton *mergeToolButton = mergeOrAbort.addButton(tr("Run &Merge Tool"),
                                                          QMessageBox::AcceptRole);
    mergeOrAbort.addButton(QMessageBox::Ignore);
    if (abortCommand == QLatin1String("rebase"))
        mergeOrAbort.addButton(tr("&Skip"), QMessageBox::RejectRole);
    if (!abortCommand.isEmpty())
        mergeOrAbort.addButton(QMessageBox::Abort);
    switch (mergeOrAbort.exec()) {
    case QMessageBox::Abort:
        synchronousAbortCommand(workingDir, abortCommand);
        break;
    case QMessageBox::Ignore:
        break;
    default: // Merge or Skip
        if (mergeOrAbort.clickedButton() == mergeToolButton) {
            merge(workingDir);
        } else if (!abortCommand.isEmpty()) {
            QStringList arguments = QStringList() << abortCommand << QLatin1String("--skip");
            executeAndHandleConflicts(workingDir, arguments, abortCommand);
        }
con's avatar
con committed
}

void GitClient::synchronousSubversionFetch(const QString &workingDirectory)
{
    QStringList args;
    args << QLatin1String("svn") << QLatin1String("fetch");
    // Disable UNIX terminals to suppress SSH prompting.
Orgad Shaneh's avatar
Orgad Shaneh committed
    const unsigned flags = VcsBasePlugin::SshPasswordPrompt
            | VcsBasePlugin::ShowStdOutInLogWindow
            | VcsBasePlugin::ShowSuccessMessage;
    synchronousGit(workingDirectory, args, flags);
}

void GitClient::subversionLog(const QString &workingDirectory)
{
    QStringList arguments;
    arguments << QLatin1String("svn") << QLatin1String("log");
    int logCount = settings()->intValue(GitSettings::logCountKey);
    if (logCount > 0)
         arguments << (QLatin1String("--limit=") + QString::number(logCount));

    // Create a command editor, no highlighting or interaction.
    const QString title = tr("Git SVN Log");
hjk's avatar
hjk committed
    const Core::Id editorId = Git::Constants::C_GIT_COMMAND_LOG_EDITOR;
hjk's avatar
hjk committed
    const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, QStringList());
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("svnLog", sourceFile);
    if (!editor)
hjk's avatar
hjk committed
        editor = createVcsEditor(editorId, title, sourceFile, CodecNone, "svnLog", sourceFile, 0);
    executeGit(workingDirectory, arguments, editor);
}

void GitClient::push(const QString &workingDirectory, const QStringList &pushArgs)
con's avatar
con committed
{
    QStringList arguments(QLatin1String("push"));
    if (!pushArgs.isEmpty())
        arguments += pushArgs;
    executeGit(workingDirectory, arguments, 0, true);
bool GitClient::synchronousMerge(const QString &workingDirectory, const QString &branch)
{
    QString command = QLatin1String("merge");
    QStringList arguments;

    arguments << command << branch;
    return executeAndHandleConflicts(workingDirectory, arguments, command);
Orgad Shaneh's avatar
Orgad Shaneh committed
bool GitClient::canRebase(const QString &workingDirectory) const
{
    const QString gitDir = findGitDirForRepository(workingDirectory);
    if (QFileInfo(gitDir + QLatin1String("/rebase-apply")).exists()
            || QFileInfo(gitDir + QLatin1String("/rebase-merge")).exists()) {
        VcsBase::VcsBaseOutputWindow::instance()->appendError(
                    tr("Rebase, merge or am is in progress. Finish "
                       "or abort it and then try again."));
Orgad Shaneh's avatar
Orgad Shaneh committed
        return false;
    }
    return true;
}

void GitClient::rebase(const QString &workingDirectory, const QString &baseBranch)
    // Git might request an editor, so this must be done asynchronously
    // and without timeout
    QString gitCommand = QLatin1String("rebase");
    arguments << gitCommand << baseBranch;
    outputWindow()->appendCommand(workingDirectory,
                                  settings()->stringValue(GitSettings::binaryPathKey),
                                  arguments);
    VcsBase::Command *command = createCommand(workingDirectory, 0, true);
    new ConflictHandler(command, workingDirectory, gitCommand);
    command->setProgressParser(new ProgressParser);
    command->addJob(arguments, -1);
    command->execute();
bool GitClient::synchronousRevert(const QString &workingDirectory, const QString &commit)
{
    QStringList arguments;
    const QString command = QLatin1String("revert");
    // Do not stash if --continue or --abort is given as the commit
    if (!commit.startsWith(QLatin1Char('-')) && !beginStashScope(workingDirectory, command))
        return false;

    arguments << command << QLatin1String("--no-edit") << commit;

    return executeAndHandleConflicts(workingDirectory, arguments, command);
}

bool GitClient::synchronousCherryPick(const QString &workingDirectory, const QString &commit)
    const QString command = QLatin1String("cherry-pick");
    // "commit" might be --continue or --abort
    const bool isRealCommit = !commit.startsWith(QLatin1Char('-'));
    if (isRealCommit && !beginStashScope(workingDirectory, command))
    QStringList arguments(command);
    if (isRealCommit && isRemoteCommit(workingDirectory, commit))
        arguments << QLatin1String("-x");
    arguments << commit;

    return executeAndHandleConflicts(workingDirectory, arguments, command);
}

void GitClient::interactiveRebase(const QString &workingDirectory, const QString &commit, bool fixup)
Orgad Shaneh's avatar
Orgad Shaneh committed
{
    QStringList arguments;
    arguments << QLatin1String("rebase") << QLatin1String("-i");
    if (fixup)
        arguments << QLatin1String("--autosquash");
    arguments << commit + QLatin1Char('^');
Orgad Shaneh's avatar
Orgad Shaneh committed
    outputWindow()->appendCommand(workingDirectory, settings()->stringValue(GitSettings::binaryPathKey), arguments);
    if (fixup)
        m_disableEditor = true;
Orgad Shaneh's avatar
Orgad Shaneh committed
    VcsBase::Command *command = createCommand(workingDirectory, 0, true);
    new ConflictHandler(command, workingDirectory, QLatin1String("rebase"));
    command->setProgressParser(new ProgressParser);
Orgad Shaneh's avatar
Orgad Shaneh committed
    command->addJob(arguments, -1);
    command->execute();
    command->setCookie(workingDirectory);
    if (fixup)
        m_disableEditor = false;
QString GitClient::msgNoChangedFiles()
{
    return tr("There are no modified files.");
}

QString GitClient::msgNoCommits(bool includeRemote)
{
    return includeRemote ? tr("No commits were found") : tr("No local commits were found");
}

void GitClient::stashPop(const QString &workingDirectory, const QString &stash)
{
    QStringList arguments(QLatin1String("stash"));
    arguments << QLatin1String("pop");
    if (!stash.isEmpty())
        arguments << stash;
    VcsBase::Command *cmd = executeGit(workingDirectory, arguments, 0, true, true);
    new ConflictHandler(cmd, workingDirectory);
void GitClient::stashPop(const QString &workingDirectory)
{
    stashPop(workingDirectory, QString());
}

bool GitClient::synchronousStashRestore(const QString &workingDirectory,
                                        const QString &stash,
                                        const QString &branch /* = QString()*/,
                                        QString *errorMessage)
{
    QStringList arguments(QLatin1String("stash"));
Tobias Hunger's avatar
Tobias Hunger committed
    if (branch.isEmpty())
        arguments << QLatin1String(pop ? "pop" : "apply") << stash;
Tobias Hunger's avatar
Tobias Hunger committed
    else
        arguments << QLatin1String("branch") << branch << stash;
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText,
                                        VcsBasePlugin::ExpectRepoChanges);
    if (!rc) {
        const QString stdErr = commandOutputFromLocal8Bit(errorText);
        const QString nativeWorkingDir = QDir::toNativeSeparators(workingDirectory);
        const QString msg = branch.isEmpty() ?
Tobias Hunger's avatar
Tobias Hunger committed
                            tr("Cannot restore stash \"%1\": %2").
                            arg(nativeWorkingDir, stdErr) :
Tobias Hunger's avatar
Tobias Hunger committed
                            tr("Cannot restore stash \"%1\" to branch \"%2\": %3").
                            arg(nativeWorkingDir, branch, stdErr);
Tobias Hunger's avatar
Tobias Hunger committed
        if (errorMessage)
            *errorMessage = msg;
Tobias Hunger's avatar
Tobias Hunger committed
        else
            outputWindow()->appendError(msg);
        return false;
    }
    QString output = commandOutputFromLocal8Bit(outputText);
    if (!output.isEmpty())
        outputWindow()->append(output);
    return true;
}

bool GitClient::synchronousStashRemove(const QString &workingDirectory,
                            const QString &stash /* = QString() */,
                            QString *errorMessage /* = 0 */)
{
    QStringList arguments(QLatin1String("stash"));
    if (stash.isEmpty())
        arguments << QLatin1String("clear");
        arguments << QLatin1String("drop") << stash;
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
    if (!rc) {
        const QString stdErr = commandOutputFromLocal8Bit(errorText);
        const QString nativeWorkingDir = QDir::toNativeSeparators(workingDirectory);
        const QString msg = stash.isEmpty() ?
Tobias Hunger's avatar
Tobias Hunger committed
                            tr("Cannot remove stashes of \"%1\": %2").
                            arg(nativeWorkingDir, stdErr) :
Tobias Hunger's avatar
Tobias Hunger committed
                            tr("Cannot remove stash \"%1\" of \"%2\": %3").
                            arg(stash, nativeWorkingDir, stdErr);
Tobias Hunger's avatar
Tobias Hunger committed
        if (errorMessage)
            *errorMessage = msg;
Tobias Hunger's avatar
Tobias Hunger committed
        else
            outputWindow()->appendError(msg);
        return false;
    }
    QString output = commandOutputFromLocal8Bit(outputText);
    if (!output.isEmpty())
        outputWindow()->append(output);
void GitClient::branchList(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("branch"));
    arguments << QLatin1String("-r") << QLatin1String(noColorOption);
    executeGit(workingDirectory, arguments, 0, true);
}

void GitClient::stashList(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("stash"));
    arguments << QLatin1String("list") << QLatin1String(noColorOption);
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
}

bool GitClient::synchronousStashList(const QString &workingDirectory,
                                     QList<Stash> *stashes,
                                     QString *errorMessage /* = 0 */)
{
    stashes->clear();
    QStringList arguments(QLatin1String("stash"));
    arguments << QLatin1String("list") << QLatin1String(noColorOption);
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
Tobias Hunger's avatar
Tobias Hunger committed
        const QString msg = tr("Cannot retrieve stash list of \"%1\": %2").
                            arg(QDir::toNativeSeparators(workingDirectory),
                                commandOutputFromLocal8Bit(errorText));
        if (errorMessage)
            *errorMessage = msg;
            outputWindow()->appendError(msg);
        return false;
    }
    Stash stash;
    foreach (const QString &line, commandOutputLinesFromLocal8Bit(outputText))
        if (stash.parseStashLine(line))
            stashes->push_back(stash);
    return true;
}

QString GitClient::readConfig(const QString &workingDirectory, const QStringList &configVar) const
con's avatar
con committed
{
    QStringList arguments;
    arguments << QLatin1String("config") << configVar;

    QByteArray outputText;
    if (!fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText,
                             VcsBasePlugin::SuppressCommandLogging))
        return QString();
    if (Utils::HostOsInfo::isWindowsHost())
        return Utils::SynchronousProcess::normalizeNewlines(QString::fromUtf8(outputText));
    return commandOutputFromLocal8Bit(outputText);
con's avatar
con committed
}

// Read a single-line config value, return trimmed
QString GitClient::readConfigValue(const QString &workingDirectory, const QString &configVar) const
con's avatar
con committed
{
    return readConfig(workingDirectory, QStringList(configVar)).remove(QLatin1Char('\n'));
}

bool GitClient::cloneRepository(const QString &directory,const QByteArray &url)
{
    QDir workingDirectory(directory);
Orgad Shaneh's avatar
Orgad Shaneh committed
    const unsigned flags = VcsBasePlugin::SshPasswordPrompt
            | VcsBasePlugin::ShowStdOutInLogWindow
            | VcsBasePlugin::ShowSuccessMessage;

    if (workingDirectory.exists()) {
        if (!synchronousInit(workingDirectory.path()))
            return false;

        QStringList arguments(QLatin1String("remote"));
        arguments << QLatin1String("add") << QLatin1String("origin") << QLatin1String(url);
        if (!fullySynchronousGit(workingDirectory.path(), arguments, 0))
Tobias Hunger's avatar
Tobias Hunger committed
            return false;

        arguments.clear();
        arguments << QLatin1String("fetch");
Tobias Hunger's avatar
Tobias Hunger committed
        const Utils::SynchronousProcessResponse resp =
                synchronousGit(workingDirectory.path(), arguments, flags);
        if (resp.result != Utils::SynchronousProcessResponse::Finished)
            return false;

        arguments.clear();
Tobias Hunger's avatar
Tobias Hunger committed
        arguments << QLatin1String("config")
                  << QLatin1String("branch.master.remote")
                  <<  QLatin1String("origin");
        if (!fullySynchronousGit(workingDirectory.path(), arguments, 0))
Tobias Hunger's avatar
Tobias Hunger committed
        arguments << QLatin1String("config")
                  << QLatin1String("branch.master.merge")
                  << QLatin1String("refs/heads/master");
        if (!fullySynchronousGit(workingDirectory.path(), arguments, 0))
            return false;

        return true;
    } else {
        QStringList arguments(QLatin1String("clone"));
        arguments << QLatin1String(url) << workingDirectory.dirName();
Tobias Hunger's avatar
Tobias Hunger committed
        const Utils::SynchronousProcessResponse resp =
                synchronousGit(workingDirectory.path(), arguments, flags);
hjk's avatar
hjk committed
        // TODO: Turn this into a VcsBaseClient and use resetCachedVcsInfo(...)
        Core::VcsManager::resetVersionControlForDirectory(workingDirectory.absolutePath());
        return (resp.result == Utils::SynchronousProcessResponse::Finished);
    }
}

QString GitClient::vcsGetRepositoryURL(const QString &directory)
{
    QStringList arguments(QLatin1String("config"));
    QByteArray outputText;

    arguments << QLatin1String("remote.origin.url");

    if (fullySynchronousGit(directory, arguments, &outputText, 0,
                            VcsBasePlugin::SuppressCommandLogging)) {
        return commandOutputFromLocal8Bit(outputText);
GitSettings *GitClient::settings() const
// determine version as '(major << 16) + (minor << 8) + patch' or 0.
Orgad Shaneh's avatar
Orgad Shaneh committed
unsigned GitClient::gitVersion(QString *errorMessage) const
    const QString newGitBinary = gitBinaryPath();
    if (m_gitVersionForBinary != newGitBinary && !newGitBinary.isEmpty()) {
        // Do not execute repeatedly if that fails (due to git
        // not being installed) until settings are changed.
Orgad Shaneh's avatar
Orgad Shaneh committed
        m_cachedGitVersion = synchronousGitVersion(errorMessage);
        m_gitVersionForBinary = newGitBinary;

// determine version as '(major << 16) + (minor << 8) + patch' or 0.
Orgad Shaneh's avatar
Orgad Shaneh committed
unsigned GitClient::synchronousGitVersion(QString *errorMessage) const
    if (gitBinaryPath().isEmpty())
        return 0;

    // run git --version
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = fullySynchronousGit(QString(), QStringList(QLatin1String("--version")),
                                        &outputText, &errorText,
                                        VcsBasePlugin::SuppressCommandLogging);
Tobias Hunger's avatar
Tobias Hunger committed
        const QString msg = tr("Cannot determine git version: %1").arg(commandOutputFromLocal8Bit(errorText));
Orgad Shaneh's avatar
Orgad Shaneh committed
        if (errorMessage)
Orgad Shaneh's avatar
Orgad Shaneh committed
        else
            outputWindow()->appendError(msg);
        return 0;
    }
    // cut 'git version 1.6.5.1.sha'
    const QString output = commandOutputFromLocal8Bit(outputText);
    QRegExp versionPattern(QLatin1String("^[^\\d]+(\\d+)\\.(\\d+)\\.(\\d+).*$"));
    QTC_ASSERT(versionPattern.isValid(), return 0);
    QTC_ASSERT(versionPattern.exactMatch(output), return 0);
    const unsigned major = versionPattern.cap(1).toUInt(0, 16);
    const unsigned minor = versionPattern.cap(2).toUInt(0, 16);
    const unsigned patch = versionPattern.cap(3).toUInt(0, 16);
    return version(major, minor, patch);
GitClient::StashInfo::StashInfo() :
    m_client(GitPlugin::instance()->gitClient())
bool GitClient::StashInfo::init(const QString &workingDirectory, const QString &command,
                                StashFlag flag)
{
    m_workingDir = workingDirectory;
    m_flags = flag;
Orgad Shaneh's avatar
Orgad Shaneh committed
    QString errorMessage;
    QString statusOutput;
    switch (m_client->gitStatus(m_workingDir, StatusMode(NoUntracked | NoSubmodules),
                              &statusOutput, &errorMessage)) {
    case GitClient::StatusChanged:
        if (m_flags & NoPrompt)
            executeStash(command, &errorMessage);
            stashPrompt(command, statusOutput, &errorMessage);
        break;
    case GitClient::StatusUnchanged:
        m_stashResult = StashUnchanged;
        break;
    case GitClient::StatusFailed:
        m_stashResult = StashFailed;
        break;
    }
    if (m_stashResult == StashFailed)
Orgad Shaneh's avatar
Orgad Shaneh committed
        VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage);
    return !stashingFailed();
void GitClient::StashInfo::stashPrompt(const QString &command, const QString &statusOutput,
                                       QString *errorMessage)
Orgad Shaneh's avatar
Orgad Shaneh committed
    QMessageBox msgBox(QMessageBox::Question, tr("Uncommitted Changes Found"),
                       tr("What would you like to do with local changes in:")
                       + QLatin1String("\n\n\"") + m_workingDir + QLatin1Char('\"'),
Orgad Shaneh's avatar
Orgad Shaneh committed
                       QMessageBox::NoButton, Core::ICore::mainWindow());
Orgad Shaneh's avatar
Orgad Shaneh committed
    msgBox.setDetailedText(statusOutput);
    QPushButton *stashAndPopButton = msgBox.addButton(tr("Stash && Pop"), QMessageBox::AcceptRole);
    stashAndPopButton->setToolTip(tr("Stash local changes and pop when %1 finishes.").arg(command));
Orgad Shaneh's avatar
Orgad Shaneh committed
    QPushButton *stashButton = msgBox.addButton(tr("Stash"), QMessageBox::AcceptRole);
    stashButton->setToolTip(tr("Stash local changes and execute %1.").arg(command));
Orgad Shaneh's avatar
Orgad Shaneh committed
    QPushButton *discardButton = msgBox.addButton(tr("Discard"), QMessageBox::AcceptRole);
    discardButton->setToolTip(tr("Discard (reset) local changes and execute %1.").arg(command));

    QPushButton *ignoreButton = 0;
    if (m_flags & AllowUnstashed) {
Orgad Shaneh's avatar
Orgad Shaneh committed
        ignoreButton = msgBox.addButton(QMessageBox::Ignore);
        ignoreButton->setToolTip(tr("Execute %1 with local changes in working directory.")
                                 .arg(command));
Orgad Shaneh's avatar
Orgad Shaneh committed
    QPushButton *cancelButton = msgBox.addButton(QMessageBox::Cancel);
    cancelButton->setToolTip(tr("Cancel %1.").arg(command));
Orgad Shaneh's avatar
Orgad Shaneh committed
    msgBox.exec();
Orgad Shaneh's avatar
Orgad Shaneh committed
    if (msgBox.clickedButton() == discardButton) {
        m_stashResult = m_client->synchronousReset(m_workingDir, QStringList(), errorMessage) ?
                        StashUnchanged : StashFailed;
Orgad Shaneh's avatar
Orgad Shaneh committed
    } else if (msgBox.clickedButton() == ignoreButton) { // At your own risk, so.
        m_stashResult = NotStashed;
Orgad Shaneh's avatar
Orgad Shaneh committed
    } else if (msgBox.clickedButton() == cancelButton) {
        m_stashResult = StashCanceled;
Orgad Shaneh's avatar
Orgad Shaneh committed
    } else if (msgBox.clickedButton() == stashButton) {
        m_stashResult = m_client->executeSynchronousStash(m_workingDir,
                        creatorStashMessage(command), errorMessage) ? StashUnchanged : StashFailed;
    } else if (msgBox.clickedButton() == stashAndPopButton) {
        executeStash(command, errorMessage);
void GitClient::StashInfo::executeStash(const QString &command, QString *errorMessage)
    m_message = creatorStashMessage(command);
    if (!m_client->executeSynchronousStash(m_workingDir, m_message, errorMessage))
        m_stashResult = StashFailed;
        m_stashResult = Stashed;
bool GitClient::StashInfo::stashingFailed() const
    switch (m_stashResult) {
    case StashCanceled:
    case StashFailed:
Orgad Shaneh's avatar
Orgad Shaneh committed
        return true;
    case NotStashed:
        return !(m_flags & AllowUnstashed);
Orgad Shaneh's avatar
Orgad Shaneh committed
    default:
        return false;
    }
}

void GitClient::StashInfo::end()
{
    if (m_stashResult == Stashed) {
        QString stashName;
        if (m_client->stashNameFromMessage(m_workingDir, m_message, &stashName))
            m_client->stashPop(m_workingDir, stashName);
    }
    m_stashResult = NotStashed;
}
} // namespace Internal
} // namespace Git
Friedemann Kleint's avatar
Friedemann Kleint committed

#include "gitclient.moc"