gitclient.cpp 38.2 KB
Newer Older
1
/**************************************************************************
con's avatar
con committed
2 3 4
**
** This file is part of Qt Creator
**
5
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
con's avatar
con committed
6
**
7
** Contact: Nokia Corporation (qt-info@nokia.com)
con's avatar
con committed
8
**
9
** Commercial Usage
10
**
11 12 13 14
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
15
**
16
** GNU Lesser General Public License Usage
17
**
18 19 20 21 22 23
** Alternatively, 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.
24
**
25
** If you are unsure which license is appropriate for your use, please
hjk's avatar
hjk committed
26
** contact the sales department at http://qt.nokia.com/contact.
con's avatar
con committed
27
**
28
**************************************************************************/
hjk's avatar
hjk committed
29

con's avatar
con committed
30
#include "gitclient.h"
31
#include "gitcommand.h"
hjk's avatar
hjk committed
32

con's avatar
con committed
33
#include "commitdata.h"
hjk's avatar
hjk committed
34 35
#include "gitconstants.h"
#include "gitplugin.h"
36
#include "gitsubmiteditor.h"
37
#include "gitversioncontrol.h"
con's avatar
con committed
38

39
#include <coreplugin/actionmanager/actionmanager.h>
hjk's avatar
hjk committed
40
#include <coreplugin/coreconstants.h>
con's avatar
con committed
41
#include <coreplugin/editormanager/editormanager.h>
hjk's avatar
hjk committed
42 43
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
44
#include <coreplugin/progressmanager/progressmanager.h>
hjk's avatar
hjk committed
45
#include <coreplugin/uniqueidmanager.h>
46
#include <coreplugin/filemanager.h>
47 48 49
#include <coreplugin/filemanager.h>
#include <coreplugin/iversioncontrol.h>

con's avatar
con committed
50
#include <texteditor/itexteditor.h>
hjk's avatar
hjk committed
51 52
#include <utils/qtcassert.h>
#include <vcsbase/vcsbaseeditor.h>
53
#include <vcsbase/vcsbaseoutputwindow.h>
con's avatar
con committed
54

55 56
#include <projectexplorer/environment.h>

con's avatar
con committed
57 58
#include <QtCore/QRegExp>
#include <QtCore/QTemporaryFile>
59
#include <QtCore/QTime>
60 61
#include <QtCore/QFileInfo>
#include <QtCore/QDir>
62
#include <QtCore/QSignalMapper>
con's avatar
con committed
63

64
#include <QtGui/QMainWindow> // for msg box parent
hjk's avatar
hjk committed
65
#include <QtGui/QMessageBox>
66
#include <QtGui/QPushButton>
con's avatar
con committed
67 68 69 70

using namespace Git;
using namespace Git::Internal;

71 72
static const char *const kGitDirectoryC = ".git";
static const char *const kBranchIndicatorC = "# On branch";
con's avatar
con committed
73 74 75 76 77 78 79 80 81 82 83

static inline QString msgServerFailure()
{
    return GitClient::tr(
"Note that the git plugin for QtCreator is not able to interact with the server "
"so far. Thus, manual ssh-identification etc. will not work.");
}

inline Core::IEditor* locateEditor(const Core::ICore *core, const char *property, const QString &entry)
{
    foreach (Core::IEditor *ed, core->editorManager()->openedEditors())
84
        if (ed->file()->property(property).toString() == entry)
con's avatar
con committed
85 86 87 88
            return ed;
    return 0;
}

89 90 91 92 93 94 95 96 97 98 99
static inline QString msgRepositoryNotFound(const QString &dir)
{
    return GitClient::tr("Unable to determine the repository for %1.").arg(dir);
}

static inline QString msgParseFilesFailed()
{
    return  GitClient::tr("Unable to parse the file output.");
}

// Format a command for the status window
100 101
static QString formatCommand(const QString &binary, const QStringList &args)
{
102 103
    //: Executing: <executable> <arguments>
    return GitClient::tr("Executing: %1 %2\n").arg(binary, args.join(QString(QLatin1Char(' '))));
104 105
}

106
// ---------------- GitClient
107 108
GitClient::GitClient(GitPlugin* plugin)
  : m_msgWait(tr("Waiting for data...")),
con's avatar
con committed
109
    m_plugin(plugin),
110 111
    m_core(Core::ICore::instance()),
    m_repositoryChangedSignalMapper(0)
con's avatar
con committed
112
{
113
    if (QSettings *s = m_core->settings()) {
114
        m_settings.fromSettings(s);
115 116
        m_binaryPath = m_settings.gitBinaryPath();
    }
con's avatar
con committed
117 118 119 120 121 122
}

GitClient::~GitClient()
{
}

123 124
const char *GitClient::noColorOption = "--no-color";

con's avatar
con committed
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
QString GitClient::findRepositoryForFile(const QString &fileName)
{
    const QString gitDirectory = QLatin1String(kGitDirectoryC);
    const QFileInfo info(fileName);
    QDir dir = info.absoluteDir();
    do {
        if (dir.entryList(QDir::AllDirs|QDir::Hidden).contains(gitDirectory))
            return dir.absolutePath();
    } while (dir.cdUp());

    return QString();
}

QString GitClient::findRepositoryForDirectory(const QString &dir)
{
    const QString gitDirectory = QLatin1String(kGitDirectoryC);
    QDir directory(dir);
    do {
        if (directory.entryList(QDir::AllDirs|QDir::Hidden).contains(gitDirectory))
            return directory.absolutePath();
    } while (directory.cdUp());

    return QString();
}

// Return source file or directory string depending on parameters
// ('git diff XX' -> 'XX' , 'git diff XX file' -> 'XX/file').
static QString source(const QString &workingDirectory, const QString &fileName)
{
    if (fileName.isEmpty())
        return workingDirectory;
    QString rc = workingDirectory;
    if (!rc.isEmpty() && !rc.endsWith(QDir::separator()))
        rc += QDir::separator();
    rc += fileName;
    return rc;
}

/* Create an editor associated to VCS output of a source file/directory
 * (using the file's codec). Makes use of a dynamic property to find an
 * existing instance and to reuse it (in case, say, 'git diff foo' is
 * already open). */
VCSBase::VCSBaseEditor
    *GitClient::createVCSEditor(const QString &kind,
                                QString title,
                                // Source file or directory
                                const QString &source,
                                bool setSourceCodec,
                                // Dynamic property and value to identify that editor
                                const char *registerDynamicProperty,
                                const QString &dynamicPropertyValue) const
{
    VCSBase::VCSBaseEditor *rc = 0;
    Core::IEditor* outputEditor = locateEditor(m_core, registerDynamicProperty, dynamicPropertyValue);
    if (outputEditor) {
         // Exists already
        outputEditor->createNew(m_msgWait);
        rc = VCSBase::VCSBaseEditor::getVcsBaseEditor(outputEditor);
hjk's avatar
hjk committed
183
        QTC_ASSERT(rc, return 0);
con's avatar
con committed
184 185
    } else {
        // Create new, set wait message, set up with source and codec
con's avatar
con committed
186
        outputEditor = m_core->editorManager()->openEditorWithContents(kind, &title, m_msgWait);
187
        outputEditor->file()->setProperty(registerDynamicProperty, dynamicPropertyValue);
con's avatar
con committed
188
        rc = VCSBase::VCSBaseEditor::getVcsBaseEditor(outputEditor);
hjk's avatar
hjk committed
189
        QTC_ASSERT(rc, return 0);
con's avatar
con committed
190 191
        rc->setSource(source);
        if (setSourceCodec)
hjk's avatar
hjk committed
192
            rc->setCodec(VCSBase::VCSBaseEditor::getCodec(source));
con's avatar
con committed
193
    }
mae's avatar
mae committed
194
    m_core->editorManager()->activateEditor(outputEditor);
con's avatar
con committed
195 196 197
    return rc;
}

198 199
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
200 201
                     const QStringList &unstagedFileNames,
                     const QStringList &stagedFileNames)
con's avatar
con committed
202 203
{

204 205 206 207
    if (Git::Constants::debug)
        qDebug() << "diff" << workingDirectory << unstagedFileNames << stagedFileNames;

    const QString binary = QLatin1String(Constants::GIT_BINARY);
con's avatar
con committed
208 209 210 211 212
    const QString kind = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_KIND);
    const QString title = tr("Git Diff");

    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, workingDirectory, true, "originalFileName", workingDirectory);

213 214 215 216 217
    // Create a batch of 2 commands to be run after each other in case
    // we have a mixture of staged/unstaged files as is the case
    // when using the submit dialog.
    GitCommand *command = createCommand(workingDirectory, editor);
    // Directory diff?
218 219
    QStringList commonDiffArgs;
    commonDiffArgs << QLatin1String("diff") << QLatin1String(noColorOption);
220
    if (unstagedFileNames.empty() && stagedFileNames.empty()) {
221 222
       QStringList arguments(commonDiffArgs);
       arguments << diffArgs;
223
       VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(binary, arguments));
224
       command->addJob(arguments, m_settings.timeout);
225 226 227
    } else {
        // Files diff.
        if (!unstagedFileNames.empty()) {
228 229
           QStringList arguments(commonDiffArgs);
           arguments << QLatin1String("--") << unstagedFileNames;
230
           VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(binary, arguments));
231
           command->addJob(arguments, m_settings.timeout);
232 233
        }
        if (!stagedFileNames.empty()) {
234 235
           QStringList arguments(commonDiffArgs);
           arguments << QLatin1String("--cached") << diffArgs << QLatin1String("--") << stagedFileNames;
236
           VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(binary, arguments));
237
           command->addJob(arguments, m_settings.timeout);
238 239 240
        }
    }
    command->execute();
con's avatar
con committed
241 242
}

243 244 245
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
                     const QString &fileName)
con's avatar
con committed
246 247 248 249
{
    if (Git::Constants::debug)
        qDebug() << "diff" << workingDirectory << fileName;
    QStringList arguments;
250
    arguments << QLatin1String("diff") << QLatin1String(noColorOption);
con's avatar
con committed
251
    if (!fileName.isEmpty())
252
        arguments << diffArgs  << QLatin1String("--") << fileName;
con's avatar
con committed
253 254 255 256 257 258

    const QString kind = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_KIND);
    const QString title = tr("Git Diff %1").arg(fileName);
    const QString sourceFile = source(workingDirectory, fileName);

    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, true, "originalFileName", sourceFile);
259
    executeGit(workingDirectory, arguments, editor);
con's avatar
con committed
260 261 262 263
}

void GitClient::status(const QString &workingDirectory)
{
264
    // @TODO: Use "--no-color" once it is supported
265 266
    QStringList statusArgs(QLatin1String("status"));
    statusArgs << QLatin1String("-u");
267
    executeGit(workingDirectory, statusArgs, 0, true);
con's avatar
con committed
268 269 270 271 272 273 274
}

void GitClient::log(const QString &workingDirectory, const QString &fileName)
{
    if (Git::Constants::debug)
        qDebug() << "log" << workingDirectory << fileName;

275 276
    QStringList arguments;
    arguments << QLatin1String("log") << QLatin1String(noColorOption);
277 278 279 280

    if (m_settings.logCount > 0)
         arguments << QLatin1String("-n") << QString::number(m_settings.logCount);

con's avatar
con committed
281 282 283 284 285 286 287
    if (!fileName.isEmpty())
        arguments << fileName;

    const QString title = tr("Git Log %1").arg(fileName);
    const QString kind = QLatin1String(Git::Constants::GIT_LOG_EDITOR_KIND);
    const QString sourceFile = source(workingDirectory, fileName);
    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, false, "logFileName", sourceFile);
288
    executeGit(workingDirectory, arguments, editor);
con's avatar
con committed
289 290 291 292 293 294
}

void GitClient::show(const QString &source, const QString &id)
{
    if (Git::Constants::debug)
        qDebug() << "show" << source << id;
295 296
    QStringList arguments;
    arguments << QLatin1String("show") << QLatin1String(noColorOption) << id;
con's avatar
con committed
297 298 299 300 301 302 303

    const QString title =  tr("Git Show %1").arg(id);
    const QString kind = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_KIND);
    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, source, true, "show", id);

    const QFileInfo sourceFi(source);
    const QString workDir = sourceFi.isDir() ? sourceFi.absoluteFilePath() : sourceFi.absolutePath();
304
    executeGit(workDir, arguments, editor);
con's avatar
con committed
305 306
}

307
void GitClient::blame(const QString &workingDirectory, const QString &fileName, int lineNumber /* = -1 */)
con's avatar
con committed
308 309
{
    if (Git::Constants::debug)
310
        qDebug() << "blame" << workingDirectory << fileName << lineNumber;
con's avatar
con committed
311
    QStringList arguments(QLatin1String("blame"));
312
    arguments << QLatin1String("--") << fileName;
con's avatar
con committed
313 314 315 316 317 318

    const QString kind = QLatin1String(Git::Constants::GIT_BLAME_EDITOR_KIND);
    const QString title = tr("Git Blame %1").arg(fileName);
    const QString sourceFile = source(workingDirectory, fileName);

    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, true, "blameFileName", sourceFile);
319
    executeGit(workingDirectory, arguments, editor, false, GitCommand::NoReport, lineNumber);
con's avatar
con committed
320 321
}

322 323 324 325
void GitClient::checkoutBranch(const QString &workingDirectory, const QString &branch)
{
    QStringList arguments(QLatin1String("checkout"));
    arguments <<  branch;
326 327
    GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
    connectRepositoryChanged(workingDirectory, cmd);
328 329
}

con's avatar
con committed
330 331 332 333 334 335 336 337 338 339 340
void GitClient::checkout(const QString &workingDirectory, const QString &fileName)
{
    // Passing an empty argument as the file name is very dangereous, since this makes
    // git checkout apply to all files. Almost looks like a bug in git.
    if (fileName.isEmpty())
        return;

    QStringList arguments;
    arguments << QLatin1String("checkout") << QLatin1String("HEAD") << QLatin1String("--")
            << fileName;

341
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
342 343 344 345 346 347 348 349 350
}

void GitClient::hardReset(const QString &workingDirectory, const QString &commit)
{
    QStringList arguments;
    arguments << QLatin1String("reset") << QLatin1String("--hard");
    if (!commit.isEmpty())
        arguments << commit;

351 352
    GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
    connectRepositoryChanged(workingDirectory, cmd);
con's avatar
con committed
353 354 355 356 357 358 359
}

void GitClient::addFile(const QString &workingDirectory, const QString &fileName)
{
    QStringList arguments;
    arguments << QLatin1String("add") << fileName;

360
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
361 362 363 364
}

bool GitClient::synchronousAdd(const QString &workingDirectory, const QStringList &files)
{
365 366
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory << files;
con's avatar
con committed
367 368 369
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
370
    arguments << QLatin1String("add") << files;
con's avatar
con committed
371 372 373 374
    const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText);
    if (!rc) {
        const QString errorMessage = tr("Unable to add %n file(s) to %1: %2", 0, files.size()).
                                     arg(workingDirectory, QString::fromLocal8Bit(errorText));
375
        VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage);
con's avatar
con committed
376 377 378 379
    }
    return rc;
}

380 381
bool GitClient::synchronousReset(const QString &workingDirectory,
                                 const QStringList &files)
382 383 384
{
    QString errorMessage;
    const bool rc = synchronousReset(workingDirectory, files, &errorMessage);
385 386
    if (!rc)
        VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage);
387 388 389 390 391 392
    return rc;
}

bool GitClient::synchronousReset(const QString &workingDirectory,
                                 const QStringList &files,
                                 QString *errorMessage)
393 394 395 396 397 398
{
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory << files;
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
399
    arguments << QLatin1String("reset") << QLatin1String("HEAD") << QLatin1String("--") << files;
400 401
    const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText);
    const QString output = QString::fromLocal8Bit(outputText);
402
    VCSBase::VCSBaseOutputWindow::instance()->append(output);
403 404 405
    // Note that git exits with 1 even if the operation is successful
    // Assume real failure if the output does not contain "foo.cpp modified"
    if (!rc && !output.contains(QLatin1String("modified"))) {
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
        *errorMessage = tr("Unable to reset %n file(s) in %1: %2", 0, files.size()).arg(workingDirectory, QString::fromLocal8Bit(errorText));
        return false;
    }
    return true;
}

bool GitClient::synchronousCheckout(const QString &workingDirectory,
                                    const QStringList &files,
                                    QString *errorMessage)
{
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory << files;
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
    arguments << QLatin1String("checkout") << QLatin1String("--") << files;
    const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText);
    if (!rc) {
        *errorMessage = tr("Unable to checkout %n file(s) in %1: %2", 0, files.size()).arg(workingDirectory, QString::fromLocal8Bit(errorText));
425 426 427 428 429
        return false;
    }
    return true;
}

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
bool GitClient::synchronousStash(const QString &workingDirectory, QString *errorMessage)
{
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory;
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
    arguments << QLatin1String("stash");
    const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText);
    if (!rc) {
        *errorMessage = tr("Unable stash in %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText));
        return false;
    }
    return true;
}

446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs,
                                     QString *output, QString *errorMessage)
{
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory << branchArgs;
    branchArgs.push_front(QLatin1String("branch"));
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = synchronousGit(workingDirectory, branchArgs, &outputText, &errorText);
    if (!rc) {
        *errorMessage = tr("Unable to run branch command: %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText));
        return false;
    }
    *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r'));
    return true;
}

bool GitClient::synchronousShow(const QString &workingDirectory, const QString &id,
                                 QString *output, QString *errorMessage)
{
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory << id;
    QStringList args(QLatin1String("show"));
469
    args << QLatin1String(noColorOption) << id;
470 471 472 473 474 475 476 477 478 479 480
    QByteArray outputText;
    QByteArray errorText;
    const bool rc = synchronousGit(workingDirectory, args, &outputText, &errorText);
    if (!rc) {
        *errorMessage = tr("Unable to run show: %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText));
        return false;
    }
    *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r'));
    return true;
}

481 482 483
// Factory function to create an asynchronous command
GitCommand *GitClient::createCommand(const QString &workingDirectory,
                             VCSBase::VCSBaseEditor* editor,
484 485
                             bool outputToWindow,
                             int editorLineNumber)
con's avatar
con committed
486 487
{
    if (Git::Constants::debug)
488
        qDebug() << Q_FUNC_INFO << workingDirectory << editor;
489

490
    VCSBase::VCSBaseOutputWindow *outputWindow = VCSBase::VCSBaseOutputWindow::instance();
491 492 493
    GitCommand* command = new GitCommand(binary(), workingDirectory, processEnvironment(), QVariant(editorLineNumber));
    if (editor)
        connect(command, SIGNAL(finished(bool,QVariant)), editor, SLOT(commandFinishedGotoLine(bool,QVariant)));
con's avatar
con committed
494
    if (outputToWindow) {
495 496
        if (editor) { // assume that the commands output is the important thing
            connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendDataSilently(QByteArray)));
497 498 499
        } else {
            connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendData(QByteArray)));
        }
con's avatar
con committed
500
    } else {
hjk's avatar
hjk committed
501
        QTC_ASSERT(editor, /**/);
502
        connect(command, SIGNAL(outputData(QByteArray)), editor, SLOT(setPlainTextDataFiltered(QByteArray)));
con's avatar
con committed
503 504 505
    }

    if (outputWindow)
506
        connect(command, SIGNAL(errorText(QString)), outputWindow, SLOT(appendError(QString)));
507 508
    return command;
}
con's avatar
con committed
509

510
// Execute a single command
511 512 513 514 515 516
GitCommand *GitClient::executeGit(const QString &workingDirectory,
                                  const QStringList &arguments,
                                  VCSBase::VCSBaseEditor* editor,
                                  bool outputToWindow,
                                  GitCommand::TerminationReportMode tm,
                                  int editorLineNumber)
517
{
518
    VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(QLatin1String(Constants::GIT_BINARY), arguments));
519
    GitCommand *command = createCommand(workingDirectory, editor, outputToWindow, editorLineNumber);
520
    command->addJob(arguments, m_settings.timeout);
521
    command->setTerminationReportMode(tm);
522
    command->execute();
523
    return command;
con's avatar
con committed
524 525
}

526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
// Return fixed arguments required to run
QStringList GitClient::binary() const
{
#ifdef Q_OS_WIN
        QStringList args;
        args << QLatin1String("cmd.exe") << QLatin1String("/c") << m_binaryPath;
        return args;
#else
        return QStringList(m_binaryPath);
#endif
}

QStringList GitClient::processEnvironment() const
{
    ProjectExplorer::Environment environment = ProjectExplorer::Environment::systemEnvironment();
    if (m_settings.adoptPath)
        environment.set(QLatin1String("PATH"), m_settings.path);
    return environment.toStringList();
}

546
bool GitClient::synchronousGit(const QString &workingDirectory,
547
                               const QStringList &gitArguments,
548 549 550
                               QByteArray* outputText,
                               QByteArray* errorText,
                               bool logCommandToWindow)
con's avatar
con committed
551 552
{
    if (Git::Constants::debug)
553
        qDebug() << "synchronousGit" << workingDirectory << gitArguments;
con's avatar
con committed
554

555
    if (logCommandToWindow)
556
        VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(m_binaryPath, gitArguments));
557 558

    QProcess process;
con's avatar
con committed
559
    process.setWorkingDirectory(workingDirectory);
560
    process.setEnvironment(processEnvironment());
con's avatar
con committed
561

562 563 564
    QStringList args = binary();
    const QString executable = args.front();
    args.pop_front();
565 566
    args.append(gitArguments);
    process.start(executable, args);
567
    process.closeWriteChannel();
568

con's avatar
con committed
569 570 571
    if (!process.waitForFinished()) {
        if (errorText)
            *errorText = "Error: Git timed out";
572
        process.kill();
con's avatar
con committed
573 574 575 576 577 578 579 580 581 582 583 584 585 586
        return false;
    }

    if (outputText)
        *outputText = process.readAllStandardOutput();

    if (errorText)
        *errorText = process.readAllStandardError();

    if (Git::Constants::debug)
        qDebug() << "synchronousGit ex=" << process.exitCode();
    return process.exitCode() == 0;
}

587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
static inline int
        askWithDetailedText(QWidget *parent,
                            const QString &title, const QString &msg,
                            const QString &inf,
                            QMessageBox::StandardButton defaultButton,
                            QMessageBox::StandardButtons buttons = QMessageBox::Yes|QMessageBox::No)
{
    QMessageBox msgBox(QMessageBox::Question, title, msg, buttons, parent);
    msgBox.setDetailedText(inf);
    msgBox.setDefaultButton(defaultButton);
    return msgBox.exec();
}

// Convenience that pops up an msg box.
GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory)
{
    QString errorMessage;
    const StashResult sr = ensureStash(workingDirectory, &errorMessage);
605 606
    if (sr == StashFailed)
        VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage);
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
    return sr;
}

// Ensure that changed files are stashed before a pull or similar
GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory, QString *errorMessage)
{
    QString statusOutput;
    switch (gitStatus(workingDirectory, false, &statusOutput, errorMessage)) {
        case StatusChanged:
        break;
        case StatusUnchanged:
        return StashUnchanged;
        case StatusFailed:
        return StashFailed;
    }

    const int answer = askWithDetailedText(m_core->mainWindow(), tr("Changes"),
                             tr("You have modified files. Would you like to stash your changes?"),
                             statusOutput, QMessageBox::Yes, QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel);
    switch (answer) {
        case QMessageBox::Cancel:
            return StashCanceled;
        case QMessageBox::Yes:
            if (!synchronousStash(workingDirectory, errorMessage))
                return StashFailed;
            break;
        case QMessageBox::No: // At your own risk, so.
            return NotStashed;
        }

    return Stashed;
 }
639 640 641 642 643 644 645 646 647

// 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;
648
        for ( ; fileSpec.at(nonBlankPos).isSpace(); nonBlankPos++) ;
649 650 651 652 653 654
        if (nonBlankPos > filePos)
            fileSpec.remove(filePos, nonBlankPos - filePos);
    }
    return fileSpec;
}

655 656 657 658 659 660 661 662
GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory,
                                             bool untracked,
                                             QString *output,
                                             QString *errorMessage)
{
    // Run 'status'. Note that git returns exitcode 1 if there are no added files.
    QByteArray outputText;
    QByteArray errorText;
663
    // @TODO: Use "--no-color" once it is supported
664 665 666 667
    QStringList statusArgs(QLatin1String("status"));
    if (untracked)
        statusArgs << QLatin1String("-u");
    const bool statusRc = synchronousGit(workingDirectory, statusArgs, &outputText, &errorText);
668
    GitCommand::removeColorCodes(&outputText);
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
    if (output)
        *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r'));
    // Is it something really fatal?
    if (!statusRc && !outputText.contains(kBranchIndicatorC)) {
        if (errorMessage) {
            const QString error = QString::fromLocal8Bit(errorText).remove(QLatin1Char('\r'));
            *errorMessage = tr("Unable to obtain the status: %1").arg(error);
        }
        return StatusFailed;
    }
    // Unchanged?
    if (outputText.contains("nothing to commit"))
        return StatusUnchanged;
    return StatusChanged;
}

con's avatar
con committed
685 686 687 688 689
bool GitClient::getCommitData(const QString &workingDirectory,
                              QString *commitTemplate,
                              CommitData *d,
                              QString *errorMessage)
{
690 691 692
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory;

con's avatar
con committed
693 694 695 696 697
    d->clear();

    // Find repo
    const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
    if (repoDirectory.isEmpty()) {
698
        *errorMessage = msgRepositoryNotFound(workingDirectory);
con's avatar
con committed
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
        return false;
    }

    d->panelInfo.repository = repoDirectory;

    QDir gitDir(repoDirectory);
    if (!gitDir.cd(QLatin1String(kGitDirectoryC))) {
        *errorMessage = tr("The repository %1 is not initialized yet.").arg(repoDirectory);
        return false;
    }

    // Read description
    const QString descriptionFile = gitDir.absoluteFilePath(QLatin1String("description"));
    if (QFileInfo(descriptionFile).isFile()) {
        QFile file(descriptionFile);
        if (file.open(QIODevice::ReadOnly|QIODevice::Text))
            d->panelInfo.description = QString::fromLocal8Bit(file.readAll()).trimmed();
    }

    // Run status. Note that it has exitcode 1 if there are no added files.
719
    QString output;
720
    switch (gitStatus(repoDirectory, true, &output, errorMessage)) {
721 722 723
    case  StatusChanged:
        break;
    case StatusUnchanged:
724
        *errorMessage = msgNoChangedFiles();
725 726 727
        return false;
    case StatusFailed:
        return false;
con's avatar
con committed
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747
    }

    //    Output looks like:
    //    # On branch [branchname]
    //    # Changes to be committed:
    //    #   (use "git reset HEAD <file>..." to unstage)
    //    #
    //    #       modified:   somefile.cpp
    //    #       new File:   somenew.h
    //    #
    //    # Changed but not updated:
    //    #   (use "git add <file>..." to update what will be committed)
    //    #
    //    #       modified:   someother.cpp
    //    #
    //    # Untracked files:
    //    #   (use "git add <file>..." to include in what will be committed)
    //    #
    //    #       list of files...

748
    if (!d->parseFilesFromStatus(output)) {
749
        *errorMessage = msgParseFilesFailed();
con's avatar
con committed
750 751
        return false;
    }
752
    // Filter out untracked files that are not part of the project
753
    VCSBase::VCSBaseSubmitEditor::filterUntrackedFilesOfProject(repoDirectory, &d->untrackedFiles);
754 755 756 757
    if (d->filesEmpty()) {
        *errorMessage = msgNoChangedFiles();
        return false;
    }
con's avatar
con committed
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776

    d->panelData.author = readConfigValue(workingDirectory, QLatin1String("user.name"));
    d->panelData.email = readConfigValue(workingDirectory, QLatin1String("user.email"));

    // Get the commit template
    const QString templateFilename = readConfigValue(workingDirectory, QLatin1String("commit.template"));
    if (!templateFilename.isEmpty()) {
        QFile templateFile(templateFilename);
        if (templateFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
            *commitTemplate = QString::fromLocal8Bit(templateFile.readAll());
        } else {
            qWarning("Unable to read commit template %s: %s",
                     qPrintable(templateFilename),
                     qPrintable(templateFile.errorString()));
        }
    }
    return true;
}

777
// addAndCommit:
778
bool GitClient::addAndCommit(const QString &repositoryDirectory,
con's avatar
con committed
779 780
                             const GitSubmitEditorPanelData &data,
                             const QString &messageFile,
781
                             const QStringList &checkedFiles,
782 783
                             const QStringList &origCommitFiles,
                             const QStringList &origDeletedFiles)
con's avatar
con committed
784
{
785
    if (Git::Constants::debug)
786
        qDebug() << "GitClient::addAndCommit:" << repositoryDirectory << checkedFiles << origCommitFiles;
787 788 789 790 791

    // Do we need to reset any files that had been added before
    // (did the user uncheck any previously added files)
    const QSet<QString> resetFiles = origCommitFiles.toSet().subtract(checkedFiles.toSet());
    if (!resetFiles.empty())
792
        if (!synchronousReset(repositoryDirectory, resetFiles.toList()))
793 794
            return false;

795 796 797 798 799 800
    // Re-add all to make sure we have the latest changes, but only add those that aren't marked
    // for deletion
    QStringList addFiles = checkedFiles.toSet().subtract(origDeletedFiles.toSet()).toList();
    if (!addFiles.isEmpty())
        if (!synchronousAdd(repositoryDirectory, addFiles))
            return false;
con's avatar
con committed
801 802 803 804 805 806 807 808 809

    // Do the final commit
    QStringList args;
    args << QLatin1String("commit")
         << QLatin1String("-F") << QDir::toNativeSeparators(messageFile)
         << QLatin1String("--author") << data.authorString();

    QByteArray outputText;
    QByteArray errorText;
810
    const bool rc = synchronousGit(repositoryDirectory, args, &outputText, &errorText);
811 812 813 814 815
    if (rc) {
        VCSBase::VCSBaseOutputWindow::instance()->append(tr("Committed %n file(s).\n", 0, checkedFiles.size()));
    } else {
        VCSBase::VCSBaseOutputWindow::instance()->appendError(tr("Unable to commit %n file(s): %1\n", 0, checkedFiles.size()).arg(QString::fromLocal8Bit(errorText)));
    }
con's avatar
con committed
816 817 818
    return rc;
}

819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
/* 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
 * VCSBase plugin. */

GitClient::RevertResult GitClient::revertI(QStringList files, bool *ptrToIsDirectory, QString *errorMessage)
{
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << files;

    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, false, &output, errorMessage)) {
    case StatusChanged:
        break;
    case StatusUnchanged:
        return RevertUnchanged;
    case StatusFailed:
        return RevertFailed;
    }
856
    CommitData data;
857
    if (!data.parseFilesFromStatus(output)) {
858 859 860 861 862 863 864 865 866 867 868 869 870 871
        *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.
872 873 874
    const QString modifiedState = QLatin1String("modified");
    const QStringList allStagedFiles = data.stagedFileNames(modifiedState);
    const QStringList allUnstagedFiles = data.unstagedFileNames(modifiedState);
875 876 877 878 879 880 881 882 883 884
    // 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 (Git::Constants::debug)
885
        qDebug() << Q_FUNC_INFO << data.stagedFiles << data.unstagedFiles << allStagedFiles << allUnstagedFiles << stagedFiles << unstagedFiles;
886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914

    if (stagedFiles.empty() && unstagedFiles.empty())
        return RevertUnchanged;

    // Ask to revert (to do: Handle lists with a selection dialog)
    const QMessageBox::StandardButton answer
        = QMessageBox::question(m_core->mainWindow(),
                                tr("Revert"),
                                tr("The file has been changed. Do you want to revert it?"),
                                QMessageBox::Yes|QMessageBox::No,
                                QMessageBox::No);
    if (answer == QMessageBox::No)
        return RevertCanceled;

    // Unstage the staged files
    if (!stagedFiles.empty() && !synchronousReset(repoDirectory, stagedFiles, errorMessage))
        return RevertFailed;
    // Finally revert!
    if (!synchronousCheckout(repoDirectory, stagedFiles + unstagedFiles, errorMessage))
        return RevertFailed;
    return RevertOk;
}

void GitClient::revert(const QStringList &files)
{
    bool isDirectory;
    QString errorMessage;
    switch (revertI(files, &isDirectory, &errorMessage)) {
    case RevertOk:
915 916
        m_plugin->versionControl()->emitFilesChanged(files);
        break;
917 918 919 920
    case RevertCanceled:
        break;
    case RevertUnchanged: {
        const QString msg = (isDirectory || files.size() > 1) ? msgNoChangedFiles() : tr("The file is not modified.");
921
        VCSBase::VCSBaseOutputWindow::instance()->append(msg);
922 923 924
    }
        break;
    case RevertFailed:
925
        VCSBase::VCSBaseOutputWindow::instance()->append(errorMessage);
926 927 928 929
        break;
    }
}

con's avatar
con committed
930 931
void GitClient::pull(const QString &workingDirectory)
{
932 933
    GitCommand *cmd = executeGit(workingDirectory, QStringList(QLatin1String("pull")), 0, true, GitCommand::ReportStderr);
    connectRepositoryChanged(workingDirectory, cmd);
con's avatar
con committed
934 935 936 937
}

void GitClient::push(const QString &workingDirectory)
{
938
    executeGit(workingDirectory, QStringList(QLatin1String("push")), 0, true, GitCommand::ReportStderr);
939 940
}

941 942 943 944 945
QString GitClient::msgNoChangedFiles()
{
    return tr("There are no modified files.");
}

946 947 948 949 950 951
void GitClient::stash(const QString &workingDirectory)
{
    // Check for changes and stash
    QString errorMessage;
    switch (gitStatus(workingDirectory, false, 0, &errorMessage)) {
    case  StatusChanged:
952
        executeGit(workingDirectory, QStringList(QLatin1String("stash")), 0, true);
953 954
        break;
    case StatusUnchanged:
955
        VCSBase::VCSBaseOutputWindow::instance()->append(msgNoChangedFiles());
956 957
        break;
    case StatusFailed:
958
        VCSBase::VCSBaseOutputWindow::instance()->append(errorMessage);
959 960 961 962 963 964 965 966
        break;
    }
}

void GitClient::stashPop(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("stash"));
    arguments << QLatin1String("pop");
967 968
    GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
    connectRepositoryChanged(workingDirectory, cmd);
969 970 971 972 973
}

void GitClient::branchList(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("branch"));
974
    arguments << QLatin1String("-r") << QLatin1String(noColorOption);
975
    executeGit(workingDirectory, arguments, 0, true);
976 977 978 979 980
}

void GitClient::stashList(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("stash"));
981
    arguments << QLatin1String("list") << QLatin1String(noColorOption);
982
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
983 984 985 986 987 988 989 990
}

QString GitClient::readConfig(const QString &workingDirectory, const QStringList &configVar)
{
    QStringList arguments;
    arguments << QLatin1String("config") << configVar;

    QByteArray outputText;
991 992
    if (synchronousGit(workingDirectory, arguments, &outputText, 0, false))
        return QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r'));
con's avatar
con committed
993 994 995 996 997 998 999 1000 1001
    return QString();
}

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

1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
GitSettings GitClient::settings() const
{
    return m_settings;
}

void GitClient::setSettings(const GitSettings &s)
{
    if (s != m_settings) {
        m_settings = s;
        if (QSettings *s = m_core->settings())
            m_settings.toSettings(s);
1013
        m_binaryPath = m_settings.gitBinaryPath();
1014 1015
    }
}
1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028

void GitClient::connectRepositoryChanged(const QString & repository, GitCommand *cmd)
{
    // Bind command success termination with repository to changed signal
    if (!m_repositoryChangedSignalMapper) {
        m_repositoryChangedSignalMapper = new QSignalMapper(this);
        connect(m_repositoryChangedSignalMapper, SIGNAL(mapped(QString)),
                m_plugin->versionControl(), SIGNAL(repositoryChanged(QString)));
    }
    m_repositoryChangedSignalMapper->setMapping(cmd, repository);
    connect(cmd, SIGNAL(success()), m_repositoryChangedSignalMapper, SLOT(map()),
            Qt::QueuedConnection);
}