gitclient.cpp 38.1 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
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();
}

/* 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
170
        QTC_ASSERT(rc, return 0);
con's avatar
con committed
171
172
    } else {
        // Create new, set wait message, set up with source and codec
con's avatar
con committed
173
        outputEditor = m_core->editorManager()->openEditorWithContents(kind, &title, m_msgWait);
174
        outputEditor->file()->setProperty(registerDynamicProperty, dynamicPropertyValue);
con's avatar
con committed
175
        rc = VCSBase::VCSBaseEditor::getVcsBaseEditor(outputEditor);
hjk's avatar
hjk committed
176
        QTC_ASSERT(rc, return 0);
con's avatar
con committed
177
178
        rc->setSource(source);
        if (setSourceCodec)
hjk's avatar
hjk committed
179
            rc->setCodec(VCSBase::VCSBaseEditor::getCodec(source));
con's avatar
con committed
180
    }
mae's avatar
mae committed
181
    m_core->editorManager()->activateEditor(outputEditor);
con's avatar
con committed
182
183
184
    return rc;
}

185
186
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
187
188
                     const QStringList &unstagedFileNames,
                     const QStringList &stagedFileNames)
con's avatar
con committed
189
190
{

191
192
193
194
    if (Git::Constants::debug)
        qDebug() << "diff" << workingDirectory << unstagedFileNames << stagedFileNames;

    const QString binary = QLatin1String(Constants::GIT_BINARY);
con's avatar
con committed
195
196
197
198
    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);
199
    editor->setDiffBaseDirectory(workingDirectory);
con's avatar
con committed
200

201
202
203
204
205
    // 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?
206
207
    QStringList commonDiffArgs;
    commonDiffArgs << QLatin1String("diff") << QLatin1String(noColorOption);
208
    if (unstagedFileNames.empty() && stagedFileNames.empty()) {
209
210
       QStringList arguments(commonDiffArgs);
       arguments << diffArgs;
211
       VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(binary, arguments));
212
       command->addJob(arguments, m_settings.timeout);
213
214
215
    } else {
        // Files diff.
        if (!unstagedFileNames.empty()) {
216
217
           QStringList arguments(commonDiffArgs);
           arguments << QLatin1String("--") << unstagedFileNames;
218
           VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(binary, arguments));
219
           command->addJob(arguments, m_settings.timeout);
220
221
        }
        if (!stagedFileNames.empty()) {
222
223
           QStringList arguments(commonDiffArgs);
           arguments << QLatin1String("--cached") << diffArgs << QLatin1String("--") << stagedFileNames;
224
           VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(binary, arguments));
225
           command->addJob(arguments, m_settings.timeout);
226
227
228
        }
    }
    command->execute();
con's avatar
con committed
229
230
}

231
232
233
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
                     const QString &fileName)
con's avatar
con committed
234
235
236
237
{
    if (Git::Constants::debug)
        qDebug() << "diff" << workingDirectory << fileName;
    QStringList arguments;
238
    arguments << QLatin1String("diff") << QLatin1String(noColorOption);
con's avatar
con committed
239
    if (!fileName.isEmpty())
240
        arguments << diffArgs  << QLatin1String("--") << fileName;
con's avatar
con committed
241
242
243

    const QString kind = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_KIND);
    const QString title = tr("Git Diff %1").arg(fileName);
244
    const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileName);
con's avatar
con committed
245
    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, true, "originalFileName", sourceFile);
246
    executeGit(workingDirectory, arguments, editor);
con's avatar
con committed
247
248
249
250
}

void GitClient::status(const QString &workingDirectory)
{
251
    // @TODO: Use "--no-color" once it is supported
252
253
    QStringList statusArgs(QLatin1String("status"));
    statusArgs << QLatin1String("-u");
254
    executeGit(workingDirectory, statusArgs, 0, true);
con's avatar
con committed
255
256
}

257
void GitClient::log(const QString &workingDirectory, const QStringList &fileNames)
con's avatar
con committed
258
259
{
    if (Git::Constants::debug)
260
        qDebug() << "log" << workingDirectory << fileNames;
con's avatar
con committed
261

262
263
    QStringList arguments;
    arguments << QLatin1String("log") << QLatin1String(noColorOption);
264
265
266
267

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

268
269
    if (!fileNames.isEmpty())
        arguments.append(fileNames);
con's avatar
con committed
270

271
272
273
    const QString msgArg = fileNames.empty() ? workingDirectory :
                           fileNames.join(QString(", "));
    const QString title = tr("Git Log %1").arg(msgArg);
con's avatar
con committed
274
    const QString kind = QLatin1String(Git::Constants::GIT_LOG_EDITOR_KIND);
275
    const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileNames);
con's avatar
con committed
276
    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, false, "logFileName", sourceFile);
277
    executeGit(workingDirectory, arguments, editor);
con's avatar
con committed
278
279
280
281
282
283
}

void GitClient::show(const QString &source, const QString &id)
{
    if (Git::Constants::debug)
        qDebug() << "show" << source << id;
284
285
    QStringList arguments;
    arguments << QLatin1String("show") << QLatin1String(noColorOption) << id;
con's avatar
con committed
286
287
288
289
290
291
292

    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();
293
    executeGit(workDir, arguments, editor);
con's avatar
con committed
294
295
}

296
void GitClient::blame(const QString &workingDirectory, const QString &fileName, int lineNumber /* = -1 */)
con's avatar
con committed
297
298
{
    if (Git::Constants::debug)
299
        qDebug() << "blame" << workingDirectory << fileName << lineNumber;
con's avatar
con committed
300
    QStringList arguments(QLatin1String("blame"));
301
    arguments << QLatin1String("--") << fileName;
con's avatar
con committed
302
303
304

    const QString kind = QLatin1String(Git::Constants::GIT_BLAME_EDITOR_KIND);
    const QString title = tr("Git Blame %1").arg(fileName);
305
    const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileName);
con's avatar
con committed
306
307

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

311
312
313
314
void GitClient::checkoutBranch(const QString &workingDirectory, const QString &branch)
{
    QStringList arguments(QLatin1String("checkout"));
    arguments <<  branch;
315
316
    GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
    connectRepositoryChanged(workingDirectory, cmd);
317
318
}

con's avatar
con committed
319
320
321
322
323
324
325
326
327
328
329
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;

330
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
331
332
333
334
335
336
337
338
339
}

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

340
341
    GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
    connectRepositoryChanged(workingDirectory, cmd);
con's avatar
con committed
342
343
344
345
346
347
348
}

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

349
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
350
351
352
353
}

bool GitClient::synchronousAdd(const QString &workingDirectory, const QStringList &files)
{
354
355
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory << files;
con's avatar
con committed
356
357
358
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
359
    arguments << QLatin1String("add") << files;
con's avatar
con committed
360
361
362
363
    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));
364
        VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage);
con's avatar
con committed
365
366
367
368
    }
    return rc;
}

369
370
bool GitClient::synchronousReset(const QString &workingDirectory,
                                 const QStringList &files)
371
372
373
{
    QString errorMessage;
    const bool rc = synchronousReset(workingDirectory, files, &errorMessage);
374
375
    if (!rc)
        VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage);
376
377
378
379
380
381
    return rc;
}

bool GitClient::synchronousReset(const QString &workingDirectory,
                                 const QStringList &files,
                                 QString *errorMessage)
382
383
384
385
386
387
{
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory << files;
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
388
    arguments << QLatin1String("reset") << QLatin1String("HEAD") << QLatin1String("--") << files;
389
390
    const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText);
    const QString output = QString::fromLocal8Bit(outputText);
391
    VCSBase::VCSBaseOutputWindow::instance()->append(output);
392
393
394
    // 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"))) {
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
        *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));
414
415
416
417
418
        return false;
    }
    return true;
}

419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
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;
}

435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
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"));
458
    args << QLatin1String(noColorOption) << id;
459
460
461
462
463
464
465
466
467
468
469
    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;
}

470
471
472
// Factory function to create an asynchronous command
GitCommand *GitClient::createCommand(const QString &workingDirectory,
                             VCSBase::VCSBaseEditor* editor,
473
474
                             bool outputToWindow,
                             int editorLineNumber)
con's avatar
con committed
475
476
{
    if (Git::Constants::debug)
477
        qDebug() << Q_FUNC_INFO << workingDirectory << editor;
478

479
    VCSBase::VCSBaseOutputWindow *outputWindow = VCSBase::VCSBaseOutputWindow::instance();
480
481
482
    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
483
    if (outputToWindow) {
484
485
        if (editor) { // assume that the commands output is the important thing
            connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendDataSilently(QByteArray)));
486
487
488
        } else {
            connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendData(QByteArray)));
        }
con's avatar
con committed
489
    } else {
hjk's avatar
hjk committed
490
        QTC_ASSERT(editor, /**/);
491
        connect(command, SIGNAL(outputData(QByteArray)), editor, SLOT(setPlainTextDataFiltered(QByteArray)));
con's avatar
con committed
492
493
494
    }

    if (outputWindow)
495
        connect(command, SIGNAL(errorText(QString)), outputWindow, SLOT(appendError(QString)));
496
497
    return command;
}
con's avatar
con committed
498

499
// Execute a single command
500
501
502
503
504
505
GitCommand *GitClient::executeGit(const QString &workingDirectory,
                                  const QStringList &arguments,
                                  VCSBase::VCSBaseEditor* editor,
                                  bool outputToWindow,
                                  GitCommand::TerminationReportMode tm,
                                  int editorLineNumber)
506
{
507
    VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(QLatin1String(Constants::GIT_BINARY), arguments));
508
    GitCommand *command = createCommand(workingDirectory, editor, outputToWindow, editorLineNumber);
509
    command->addJob(arguments, m_settings.timeout);
510
    command->setTerminationReportMode(tm);
511
    command->execute();
512
    return command;
con's avatar
con committed
513
514
}

515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
// 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();
}

535
bool GitClient::synchronousGit(const QString &workingDirectory,
536
                               const QStringList &gitArguments,
537
538
539
                               QByteArray* outputText,
                               QByteArray* errorText,
                               bool logCommandToWindow)
con's avatar
con committed
540
541
{
    if (Git::Constants::debug)
542
        qDebug() << "synchronousGit" << workingDirectory << gitArguments;
con's avatar
con committed
543

544
    if (logCommandToWindow)
545
        VCSBase::VCSBaseOutputWindow::instance()->appendCommand(formatCommand(m_binaryPath, gitArguments));
546
547

    QProcess process;
con's avatar
con committed
548
    process.setWorkingDirectory(workingDirectory);
549
    process.setEnvironment(processEnvironment());
con's avatar
con committed
550

551
552
553
    QStringList args = binary();
    const QString executable = args.front();
    args.pop_front();
554
555
    args.append(gitArguments);
    process.start(executable, args);
556
    process.closeWriteChannel();
557

con's avatar
con committed
558
559
560
    if (!process.waitForFinished()) {
        if (errorText)
            *errorText = "Error: Git timed out";
561
        process.kill();
con's avatar
con committed
562
563
564
565
566
567
568
569
570
571
572
573
574
575
        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;
}

576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
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);
594
595
    if (sr == StashFailed)
        VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage);
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
    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;
 }
628
629
630
631
632
633
634
635
636

// 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;
637
        for ( ; fileSpec.at(nonBlankPos).isSpace(); nonBlankPos++) ;
638
639
640
641
642
643
        if (nonBlankPos > filePos)
            fileSpec.remove(filePos, nonBlankPos - filePos);
    }
    return fileSpec;
}

644
645
646
647
648
649
650
651
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;
652
    // @TODO: Use "--no-color" once it is supported
653
654
655
656
    QStringList statusArgs(QLatin1String("status"));
    if (untracked)
        statusArgs << QLatin1String("-u");
    const bool statusRc = synchronousGit(workingDirectory, statusArgs, &outputText, &errorText);
657
    GitCommand::removeColorCodes(&outputText);
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
    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
674
675
676
677
678
bool GitClient::getCommitData(const QString &workingDirectory,
                              QString *commitTemplate,
                              CommitData *d,
                              QString *errorMessage)
{
679
680
681
    if (Git::Constants::debug)
        qDebug() << Q_FUNC_INFO << workingDirectory;

con's avatar
con committed
682
683
684
685
686
    d->clear();

    // Find repo
    const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
    if (repoDirectory.isEmpty()) {
687
        *errorMessage = msgRepositoryNotFound(workingDirectory);
con's avatar
con committed
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
        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.
708
    QString output;
709
    switch (gitStatus(repoDirectory, true, &output, errorMessage)) {
710
711
712
    case  StatusChanged:
        break;
    case StatusUnchanged:
713
        *errorMessage = msgNoChangedFiles();
714
715
716
        return false;
    case StatusFailed:
        return false;
con's avatar
con committed
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
    }

    //    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...

737
    if (!d->parseFilesFromStatus(output)) {
738
        *errorMessage = msgParseFilesFailed();
con's avatar
con committed
739
740
        return false;
    }
741
    // Filter out untracked files that are not part of the project
742
    VCSBase::VCSBaseSubmitEditor::filterUntrackedFilesOfProject(repoDirectory, &d->untrackedFiles);
743
744
745
746
    if (d->filesEmpty()) {
        *errorMessage = msgNoChangedFiles();
        return false;
    }
con's avatar
con committed
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765

    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;
}

766
// addAndCommit:
767
bool GitClient::addAndCommit(const QString &repositoryDirectory,
con's avatar
con committed
768
769
                             const GitSubmitEditorPanelData &data,
                             const QString &messageFile,
770
                             const QStringList &checkedFiles,
771
772
                             const QStringList &origCommitFiles,
                             const QStringList &origDeletedFiles)
con's avatar
con committed
773
{
774
    if (Git::Constants::debug)
775
        qDebug() << "GitClient::addAndCommit:" << repositoryDirectory << checkedFiles << origCommitFiles;
776
777
778
779
780

    // 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())
781
        if (!synchronousReset(repositoryDirectory, resetFiles.toList()))
782
783
            return false;

784
785
786
787
788
789
    // 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
790
791
792
793

    // Do the final commit
    QStringList args;
    args << QLatin1String("commit")
794
795
796
797
798
         << QLatin1String("-F") << QDir::toNativeSeparators(messageFile);

    const QString &authorString =  data.authorString();
    if (!authorString.isEmpty())
         args << QLatin1String("--author") << authorString;
con's avatar
con committed
799
800
801

    QByteArray outputText;
    QByteArray errorText;
802
    const bool rc = synchronousGit(repositoryDirectory, args, &outputText, &errorText);
803
804
805
806
807
    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
808
809
810
    return rc;
}

811
812
813
814
815
816
817
818
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
/* 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;
    }
848
    CommitData data;
849
    if (!data.parseFilesFromStatus(output)) {
850
851
852
853
854
855
856
857
858
859
860
861
862
863
        *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.
864
865
866
    const QString modifiedState = QLatin1String("modified");
    const QStringList allStagedFiles = data.stagedFileNames(modifiedState);
    const QStringList allUnstagedFiles = data.unstagedFileNames(modifiedState);
867
868
869
870
871
872
873
874
875
876
    // 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)
877
        qDebug() << Q_FUNC_INFO << data.stagedFiles << data.unstagedFiles << allStagedFiles << allUnstagedFiles << stagedFiles << unstagedFiles;
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906

    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:
907
        m_plugin->gitVersionControl()->emitFilesChanged(files);
908
        break;
909
910
911
912
    case RevertCanceled:
        break;
    case RevertUnchanged: {
        const QString msg = (isDirectory || files.size() > 1) ? msgNoChangedFiles() : tr("The file is not modified.");
913
        VCSBase::VCSBaseOutputWindow::instance()->append(msg);
914
915
916
    }
        break;
    case RevertFailed:
917
        VCSBase::VCSBaseOutputWindow::instance()->append(errorMessage);
918
919
920
921
        break;
    }
}

con's avatar
con committed
922
923
void GitClient::pull(const QString &workingDirectory)
{
924
925
    GitCommand *cmd = executeGit(workingDirectory, QStringList(QLatin1String("pull")), 0, true, GitCommand::ReportStderr);
    connectRepositoryChanged(workingDirectory, cmd);
con's avatar
con committed
926
927
928
929
}

void GitClient::push(const QString &workingDirectory)
{
930
    executeGit(workingDirectory, QStringList(QLatin1String("push")), 0, true, GitCommand::ReportStderr);
931
932
}

933
934
935
936
937
QString GitClient::msgNoChangedFiles()
{
    return tr("There are no modified files.");
}

938
939
940
941
942
943
void GitClient::stash(const QString &workingDirectory)
{
    // Check for changes and stash
    QString errorMessage;
    switch (gitStatus(workingDirectory, false, 0, &errorMessage)) {
    case  StatusChanged:
944
        executeGit(workingDirectory, QStringList(QLatin1String("stash")), 0, true);
945
946
        break;
    case StatusUnchanged:
947
        VCSBase::VCSBaseOutputWindow::instance()->append(msgNoChangedFiles());
948
949
        break;
    case StatusFailed:
950
        VCSBase::VCSBaseOutputWindow::instance()->append(errorMessage);
951
952
953
954
955
956
957
958
        break;
    }
}

void GitClient::stashPop(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("stash"));
    arguments << QLatin1String("pop");
959
960
    GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
    connectRepositoryChanged(workingDirectory, cmd);
961
962
963
964
965
}

void GitClient::branchList(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("branch"));
966
    arguments << QLatin1String("-r") << QLatin1String(noColorOption);
967
    executeGit(workingDirectory, arguments, 0, true);
968
969
970
971
972
}

void GitClient::stashList(const QString &workingDirectory)
{
    QStringList arguments(QLatin1String("stash"));
973
    arguments << QLatin1String("list") << QLatin1String(noColorOption);
974
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
975
976
977
978
979
980
981
982
}

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

    QByteArray outputText;
983
984
    if (synchronousGit(workingDirectory, arguments, &outputText, 0, false))
        return QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r'));
con's avatar
con committed
985
986
987
988
989
990
991
992
993
    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'));
}

994
995
996
997
998
999
1000
1001
1002
1003
1004
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);
1005
        m_binaryPath = m_settings.gitBinaryPath();
1006
1007
    }
}
1008
1009
1010
1011
1012
1013
1014

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)),
1015
                m_plugin->gitVersionControl(), SIGNAL(repositoryChanged(QString)));
1016
1017
1018
1019
1020
    }
    m_repositoryChangedSignalMapper->setMapping(cmd, repository);
    connect(cmd, SIGNAL(success()), m_repositoryChangedSignalMapper, SLOT(map()),
            Qt::QueuedConnection);
}