gitclient.cpp 132 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
hjk's avatar
hjk committed
4
** Contact: http://www.qt-project.org/legal
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8
9
10
11
12
13
14
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
18
19
20
21
22
23
24
25
** 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.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
con's avatar
con committed
26
27
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
28
****************************************************************************/
hjk's avatar
hjk committed
29

con's avatar
con committed
30
#include "gitclient.h"
31
#include "gitutils.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"
38
#include "mergetool.h"
con's avatar
con committed
39

40
41
#include <vcsbase/submitfilemodel.h>

con's avatar
con committed
42
#include <coreplugin/editormanager/editormanager.h>
hjk's avatar
hjk committed
43
#include <coreplugin/icore.h>
44
#include <coreplugin/vcsmanager.h>
45
#include <coreplugin/id.h>
46
47
#include <coreplugin/iversioncontrol.h>

48
#include <utils/hostosinfo.h>
hjk's avatar
hjk committed
49
#include <utils/qtcassert.h>
50
#include <utils/qtcprocess.h>
51
#include <utils/synchronousprocess.h>
52
#include <utils/fileutils.h>
Tobias Hunger's avatar
Tobias Hunger committed
53
#include <vcsbase/command.h>
hjk's avatar
hjk committed
54
#include <vcsbase/vcsbaseeditor.h>
55
#include <vcsbase/vcsbaseeditorparameterwidget.h>
56
#include <vcsbase/vcsbaseoutputwindow.h>
57
#include <vcsbase/vcsbaseplugin.h>
con's avatar
con committed
58

59
#include <diffeditor/diffeditor.h>
60
61
#include <diffeditor/diffeditorconstants.h>

62
#include <QCoreApplication>
63
#include <QDir>
64
#include <QFileInfo>
65
#include <QHash>
66
#include <QRegExp>
67
#include <QSignalMapper>
68
#include <QTime>
con's avatar
con committed
69

70
#include <QMessageBox>
71
#include <QPushButton>
72
73
#include <QToolButton>
#include <QTextCodec>
con's avatar
con committed
74

75
static const char GIT_DIRECTORY[] = ".git";
76
static const char graphLogFormatC[] = "%h %d %an %s %ci";
77
78
static const char HEAD[] = "HEAD";

con's avatar
con committed
79

80
81
82
namespace Git {
namespace Internal {

83
84
85
86
87
class GitDiffHandler : public QObject
{
    Q_OBJECT

public:
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
    enum RevisionType {
        WorkingTree,
        Index,
        Other
    };

    struct Revision {
        Revision() : type(WorkingTree) { }
        Revision(RevisionType t) : type(t) { }
        Revision(RevisionType t, const QString &i) : type(t), id(i) { }
        RevisionType type;
        QString id; // can be sha or HEAD
        QString infoText() const
        {
            switch (type) {
            case WorkingTree: return tr("Working tree");
            case Index:       return tr("Index");
            default:          return id;
            }
        }
    };

110
    GitDiffHandler(DiffEditor::DiffEditor *editor,
jkobus's avatar
jkobus committed
111
                   const QString &gitPath,
112
113
114
115
116
117
118
119
120
121
122
123
124
                   const QString &workingDirectory,
                   const QProcessEnvironment &environment,
                   int timeout);

    // index -> working tree
    void diffFile(const QString &fileName);
    // stagedFileNames - files in index, diff will compare the state in HEAD to the one in the index
    // unstagedFileNames - diff will compare the state in the index to the one in the working tree
    void diffFiles(const QStringList &stagedFileNames, const QStringList &unstagedFileNames);
    // index -> working tree
    void diffProjects(const QStringList &projectPaths);
    // index -> working tree
    void diffRepository();
125
126
127
128
    // branch HEAD -> working tree
    void diffBranch(const QString &branchName);
    // id^ -> id
    void show(const QString &id);
129
130
131
132
133
134
135

private slots:
    void slotFileListReceived(const QByteArray &data);
    void slotFileContentsReceived(const QByteArray &data);

private:
    void collectFilesList(const QStringList &additionalArguments);
136
    void prepareForCollection();
137
138
139
140
    void collectFilesContents();
    void feedEditor();
    QString workingTreeContents(const QString &fileName) const;

141
    QPointer<DiffEditor::DiffEditor> m_editor;
142
143
144
145
146
147
    const QString m_gitPath;
    const QString m_workingDirectory;
    const QProcessEnvironment m_processEnvironment;
    const int m_timeout;
    const QString m_waitMessage;

148
149
150
151
152
    struct RevisionRange {
        RevisionRange() { }
        RevisionRange(const Revision &b, const Revision &e) : begin(b), end(e) { }
        Revision begin;
        Revision end;
153
154
    };

155
156
157
158
159
160
    // filename, revision range
    QMap<QString, QList<RevisionRange> > m_requestedRevisionRanges;
    // filename, revision, dummy
    QMap<QString, QMap<Revision, bool> > m_pendingRevisions;
    // filename, revision, contents
    QMap<QString, QMap<Revision, QString> > m_collectedRevisions;
161

162
    RevisionRange m_requestedRevisionRange;
163
164
};

165
166
167
168
169
170
171
inline bool operator<(const GitDiffHandler::Revision &rev1, const GitDiffHandler::Revision &rev2)
{
    if (rev1.type != rev2.type)
        return rev1.type < rev2.type;
    return rev1.id < rev2.id;
}

172
GitDiffHandler::GitDiffHandler(DiffEditor::DiffEditor *editor,
jkobus's avatar
jkobus committed
173
               const QString &gitPath,
174
175
176
               const QString &workingDirectory,
               const QProcessEnvironment &environment,
               int timeout)
jkobus's avatar
jkobus committed
177
178
    : m_editor(editor),
      m_gitPath(gitPath),
179
180
181
182
183
184
185
186
187
      m_workingDirectory(workingDirectory),
      m_processEnvironment(environment),
      m_timeout(timeout),
      m_waitMessage(tr("Waiting for data..."))
{
}

void GitDiffHandler::diffFile(const QString &fileName)
{
188
189
190
191
192
    m_requestedRevisionRange = RevisionRange(
                Revision(Other, QLatin1String(HEAD)),
                Revision(WorkingTree));

    collectFilesList(QStringList() << QLatin1String("--") << fileName);
193
194
195
196
}

void GitDiffHandler::diffFiles(const QStringList &stagedFileNames, const QStringList &unstagedFileNames)
{
197
198
199
200
201
202
    RevisionRange stagedRange = RevisionRange(
                Revision(Other, QLatin1String(HEAD)),
                Revision(WorkingTree));
    RevisionRange unstagedRange = RevisionRange(
                Revision(Index),
                Revision(WorkingTree));
203

204
205
206
207
208
209
210
    for (int i = 0; i < stagedFileNames.count(); i++)
        m_requestedRevisionRanges[stagedFileNames.at(i)].append(stagedRange);

    for (int i = 0; i < unstagedFileNames.count(); i++)
        m_requestedRevisionRanges[unstagedFileNames.at(i)].append(unstagedRange);

    prepareForCollection();
211
212
213
214
215
    collectFilesContents();
}

void GitDiffHandler::diffProjects(const QStringList &projectPaths)
{
216
217
218
219
    m_requestedRevisionRange = RevisionRange(
                Revision(Other, QLatin1String(HEAD)),
                Revision(WorkingTree));

220
221
222
223
224
    collectFilesList(QStringList() << QLatin1String("--") << projectPaths);
}

void GitDiffHandler::diffRepository()
{
225
226
227
228
    m_requestedRevisionRange = RevisionRange(
                Revision(Other, QLatin1String(HEAD)),
                Revision(WorkingTree));

229
230
231
    collectFilesList(QStringList());
}

232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
void GitDiffHandler::diffBranch(const QString &branchName)
{
    m_requestedRevisionRange = RevisionRange(
                Revision(Other, branchName),
                Revision(WorkingTree));

    collectFilesList(QStringList() << branchName);
}

void GitDiffHandler::show(const QString &id)
{
    Revision begin(Other, id + QLatin1Char('^'));
    Revision end(Other, id);
    m_requestedRevisionRange = RevisionRange(begin, end);

    collectFilesList(QStringList() << begin.id << end.id);
}

250
251
void GitDiffHandler::collectFilesList(const QStringList &additionalArguments)
{
jkobus's avatar
jkobus committed
252
    m_editor->clear(m_waitMessage);
253
254
255
256
257
258
259
260
261
262
263
264
265
    VcsBase::Command *command = new VcsBase::Command(m_gitPath, m_workingDirectory, m_processEnvironment);
    connect(command, SIGNAL(outputData(QByteArray)), this, SLOT(slotFileListReceived(QByteArray)));
    QStringList arguments;
    arguments << QLatin1String("diff") << QLatin1String("--name-only") << additionalArguments;
    command->addJob(arguments, m_timeout);
    command->execute();
}

void GitDiffHandler::slotFileListReceived(const QByteArray &data)
{
    if (m_editor.isNull())
        return;

jkobus's avatar
jkobus committed
266
    const QString fileList = m_editor->editorWidget()->codec()->toUnicode(data);
267
268
269
270
271
    QStringList fileNames = fileList.split(QLatin1Char('\n'), QString::SkipEmptyParts);
    fileNames.removeDuplicates();

    for (int i = 0; i < fileNames.count(); i++)
        m_requestedRevisionRanges[fileNames.at(i)].append(m_requestedRevisionRange);
272

273
    prepareForCollection();
274
275
276
    collectFilesContents();
}

277
void GitDiffHandler::prepareForCollection()
278
{
279
280
281
282
283
284
285
286
287
288
289
290
    QMap<QString, QList<RevisionRange> >::const_iterator it
            = m_requestedRevisionRanges.constBegin();
    QMap<QString, QList<RevisionRange> >::const_iterator itEnd
            = m_requestedRevisionRanges.constEnd();
    while (it != itEnd) {
        const QString fileName = it.key();
        const QList<RevisionRange> &ranges = it.value();
        for (int i = 0; i < ranges.count(); i++) {
            const RevisionRange &range = ranges.at(i);
            m_pendingRevisions[fileName][range.begin] = false;
            m_pendingRevisions[fileName][range.end] = false;
        }
291

292
293
294
        ++it;
    }
}
295

296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
void GitDiffHandler::collectFilesContents()
{
    QMap<QString, QMap<Revision, bool> >::iterator itFile
            = m_pendingRevisions.begin();
    QMap<QString, QMap<Revision, bool> >::iterator itFileEnd
            = m_pendingRevisions.end();
    while (itFile != itFileEnd) {
        const QString fileName = itFile.key();
        QMap<Revision, bool> &revisions = itFile.value();
        QMap<Revision, bool>::iterator itRev
                = revisions.begin();
        QMap<Revision, bool>::iterator itRevEnd
                = revisions.end();
        while (itRev != itRevEnd) {
            const Revision revision = itRev.key();
            if (revision.type == WorkingTree) {
                // collect file here

                m_collectedRevisions[fileName][revision] = workingTreeContents(fileName);

                itRev = revisions.erase(itRev); // iterate to the next revision
            } else {
                // prepare job here

                VcsBase::Command *command = new VcsBase::Command(m_gitPath, m_workingDirectory, m_processEnvironment);
                connect(command, SIGNAL(outputData(QByteArray)), this, SLOT(slotFileContentsReceived(QByteArray)));

                QString revisionArgument = (revision.type == Other)
                        ? revision.id : QString();
                revisionArgument += QLatin1String(":./");
                QStringList arguments;
                arguments << QLatin1String("show") << revisionArgument + fileName;
                command->addJob(arguments, m_timeout);
                command->execute();

                return;
            }
        }
334

335
        itFile = m_pendingRevisions.erase(itFile); // iterate to the next file
336
    }
337
338

    feedEditor();
339
340
341
342
343
344
345
}

void GitDiffHandler::slotFileContentsReceived(const QByteArray &data)
{
    if (m_editor.isNull())
        return;

346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
    QMap<QString, QMap<Revision, bool> >::iterator itFile
            = m_pendingRevisions.begin();
    QMap<QString, QMap<Revision, bool> >::iterator itFileEnd
            = m_pendingRevisions.end();
    if (itFile != itFileEnd) {
        const QString fileName = itFile.key();
        QMap<Revision, bool> &revisions = itFile.value();
        QMap<Revision, bool>::iterator itRev
                = revisions.begin();
        QMap<Revision, bool>::iterator itRevEnd
                = revisions.end();
        if (itRev != itRevEnd) {
            const QString contents = m_editor->editorWidget()->codec()->toUnicode(data);
            m_collectedRevisions[fileName][itRev.key()] = contents;

            itRev = revisions.erase(itRev);
            if (revisions.isEmpty())
                m_pendingRevisions.erase(itFile);
        }
    }
366
367
368
369
370
371
372
373

    collectFilesContents();
}

void GitDiffHandler::feedEditor()
{
    QList<DiffEditor::DiffEditorWidget::DiffFilesContents> list;

374
375
376
377
378
379
380
381
382
383
384
    QMap<QString, QList<RevisionRange> >::const_iterator itFile
            = m_requestedRevisionRanges.constBegin();
    QMap<QString, QList<RevisionRange> >::const_iterator itFileEnd
            = m_requestedRevisionRanges.constEnd();
    while (itFile != itFileEnd) {
        const QString fileName = itFile.key();
        const QList<RevisionRange> &ranges = itFile.value();
        for (int i = 0; i < ranges.count(); i++) {
            const Revision leftRevision = ranges.at(i).begin;
            const Revision rightRevision = ranges.at(i).end;

385
            DiffEditor::DiffEditorWidget::DiffFilesContents dfc;
386
387
388
389
            dfc.leftFileInfo = DiffEditor::DiffEditorWidget::DiffFileInfo(fileName, leftRevision.infoText());
            dfc.leftText = m_collectedRevisions[fileName][leftRevision];
            dfc.rightFileInfo = DiffEditor::DiffEditorWidget::DiffFileInfo(fileName, rightRevision.infoText());
            dfc.rightText = m_collectedRevisions[fileName][rightRevision];
390
391
            list.append(dfc);
        }
392
393

        ++itFile;
394
    }
395

jkobus's avatar
jkobus committed
396
    m_editor->setDiff(list, m_workingDirectory);
397
398
399
400
401
402
403
404
405
406
    deleteLater();
}

QString GitDiffHandler::workingTreeContents(const QString &fileName) const
{
    QDir workingDir(m_workingDirectory);
    QString absoluteFileName = workingDir.absoluteFilePath(fileName);

    QFile file(absoluteFileName);
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
jkobus's avatar
jkobus committed
407
        return m_editor->editorWidget()->codec()->toUnicode(file.readAll());
408
409
410
411
412
413
    }
    return QString();
}

///////////////////////////////////////////////////////////

hjk's avatar
hjk committed
414
class BaseGitDiffArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
415
{
Friedemann Kleint's avatar
Friedemann Kleint committed
416
    Q_OBJECT
417

418
public:
419
    BaseGitDiffArgumentsWidget(GitClient *client, const QString &directory,
420
                               const QStringList &args) :
421
        m_workingDirectory(directory),
422
        m_client(client)
423
    {
424
425
        QTC_ASSERT(!directory.isEmpty(), return);
        QTC_ASSERT(m_client, return);
426

427
428
429
        m_patienceButton = addToggleButton(QLatin1String("--patience"), tr("Patience"),
                                           tr("Use the patience algorithm for calculating the differences."));
        mapSetting(m_patienceButton, client->settings()->boolPointer(GitSettings::diffPatienceKey));
430
        m_ignoreWSButton = addToggleButton(QLatin1String("--ignore-space-change"), tr("Ignore Whitespace"),
431
432
                                           tr("Ignore whitespace only changes."));
        mapSetting(m_ignoreWSButton, m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInDiffKey));
433
434

        setBaseArguments(args);
435
436
    }

437
protected:
438
439
    QString m_workingDirectory;
    GitClient *m_client;
440
441
    QToolButton *m_patienceButton;
    QToolButton *m_ignoreWSButton;
442
443
444
445
};

class GitCommitDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
446
    Q_OBJECT
447

448
public:
449
    GitCommitDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
450
451
                                 const QStringList &args, const QStringList &unstaged,
                                 const QStringList &staged) :
452
        BaseGitDiffArgumentsWidget(client, directory, args),
453
454
455
456
        m_unstagedFileNames(unstaged),
        m_stagedFileNames(staged)
    { }

457
    void executeCommand()
458
    {
459
        m_client->diff(m_workingDirectory, arguments(), m_unstagedFileNames, m_stagedFileNames);
460
461
462
463
464
465
466
467
468
    }

private:
    const QStringList m_unstagedFileNames;
    const QStringList m_stagedFileNames;
};

class GitFileDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
469
    Q_OBJECT
470
public:
471
    GitFileDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
472
                               const QStringList &args, const QString &file) :
473
        BaseGitDiffArgumentsWidget(client, directory, args),
474
475
476
        m_fileName(file)
    { }

477
    void executeCommand()
478
    {
479
        m_client->diff(m_workingDirectory, arguments(), m_fileName);
480
481
482
483
484
485
486
487
    }

private:
    const QString m_fileName;
};

class GitBranchDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
488
    Q_OBJECT
489
public:
490
    GitBranchDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
491
                                 const QStringList &args, const QString &branch) :
492
        BaseGitDiffArgumentsWidget(client, directory, args),
493
494
495
        m_branchName(branch)
    { }

496
    void executeCommand()
497
    {
498
        m_client->diffBranch(m_workingDirectory, arguments(), m_branchName);
499
500
501
502
503
504
    }

private:
    const QString m_branchName;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
505
class GitShowArgumentsWidget : public BaseGitDiffArgumentsWidget
506
{
Friedemann Kleint's avatar
Friedemann Kleint committed
507
    Q_OBJECT
508

509
public:
510
    GitShowArgumentsWidget(Git::Internal::GitClient *client,
511
512
513
                           const QString &directory,
                           const QStringList &args,
                           const QString &id) :
Orgad Shaneh's avatar
Orgad Shaneh committed
514
        BaseGitDiffArgumentsWidget(client, directory, args),
515
516
        m_client(client),
        m_workingDirectory(directory),
517
518
        m_id(id)
    {
519
520
521
522
523
524
525
526
        QList<ComboBoxItem> prettyChoices;
        prettyChoices << ComboBoxItem(tr("oneline"), QLatin1String("oneline"))
                      << ComboBoxItem(tr("short"), QLatin1String("short"))
                      << ComboBoxItem(tr("medium"), QLatin1String("medium"))
                      << ComboBoxItem(tr("full"), QLatin1String("full"))
                      << ComboBoxItem(tr("fuller"), QLatin1String("fuller"))
                      << ComboBoxItem(tr("email"), QLatin1String("email"))
                      << ComboBoxItem(tr("raw"), QLatin1String("raw"));
527
        mapSetting(addComboBox(QStringList(QLatin1String("--pretty=%1")), prettyChoices),
528
                   m_client->settings()->intPointer(GitSettings::showPrettyFormatKey));
529
530
531
    }

    void executeCommand()
532
    {
533
        m_client->show(m_workingDirectory, m_id, arguments());
534
535
536
    }

private:
537
538
    GitClient *m_client;
    QString m_workingDirectory;
539
    QString m_id;
540
541
};

hjk's avatar
hjk committed
542
class GitBlameArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
543
{
Friedemann Kleint's avatar
Friedemann Kleint committed
544
    Q_OBJECT
545

546
public:
547
548
549
550
    GitBlameArgumentsWidget(Git::Internal::GitClient *client,
                            const QString &directory,
                            const QStringList &args,
                            const QString &revision, const QString &fileName) :
551
        m_editor(0),
552
553
        m_client(client),
        m_workingDirectory(directory),
554
555
556
        m_revision(revision),
        m_fileName(fileName)
    {
557
558
559
        mapSetting(addToggleButton(QString(), tr("Omit Date"),
                                   tr("Hide the date of a change from the output.")),
                   m_client->settings()->boolPointer(GitSettings::omitAnnotationDateKey));
560
        mapSetting(addToggleButton(QLatin1String("-w"), tr("Ignore Whitespace"),
561
562
                                   tr("Ignore whitespace only changes.")),
                   m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
563
564

        setBaseArguments(args);
565
566
    }

hjk's avatar
hjk committed
567
    void setEditor(VcsBase::VcsBaseEditorWidget *editor)
568
    {
569
        QTC_ASSERT(editor, return);
570
571
572
        m_editor = editor;
    }

573
    void executeCommand()
574
    {
575
576
577
        int line = -1;
        if (m_editor)
            line = m_editor->lineNumberOfCurrentEditor();
578
        m_client->blame(m_workingDirectory, arguments(), m_fileName, m_revision, line);
579
580
581
    }

private:
hjk's avatar
hjk committed
582
    VcsBase::VcsBaseEditorWidget *m_editor;
583
584
    GitClient *m_client;
    QString m_workingDirectory;
585
586
587
588
    QString m_revision;
    QString m_fileName;
};

589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
class GitLogArgumentsWidget : public BaseGitDiffArgumentsWidget
{
    Q_OBJECT

public:
    GitLogArgumentsWidget(Git::Internal::GitClient *client,
                          const QString &directory,
                          bool enableAnnotationContextMenu,
                          const QStringList &args,
                          const QStringList &fileNames) :
        BaseGitDiffArgumentsWidget(client, directory, args),
        m_client(client),
        m_workingDirectory(directory),
        m_enableAnnotationContextMenu(enableAnnotationContextMenu),
        m_fileNames(fileNames)
    {
605
        QToolButton *diffButton = addToggleButton(QLatin1String("--patch"), tr("Show Diff"),
606
                                              tr("Show difference."));
607
608
609
610
611
612
613
614
615
616
617
        mapSetting(diffButton, m_client->settings()->boolPointer(GitSettings::logDiffKey));
        connect(diffButton, SIGNAL(toggled(bool)), m_patienceButton, SLOT(setVisible(bool)));
        connect(diffButton, SIGNAL(toggled(bool)), m_ignoreWSButton, SLOT(setVisible(bool)));
        m_patienceButton->setVisible(diffButton->isChecked());
        m_ignoreWSButton->setVisible(diffButton->isChecked());
        QStringList graphArguments(QLatin1String("--graph"));
        graphArguments << QLatin1String("--oneline") << QLatin1String("--topo-order");
        graphArguments << (QLatin1String("--pretty=format:") + QLatin1String(graphLogFormatC));
        QToolButton *graphButton = addToggleButton(graphArguments, tr("Graph"),
                                              tr("Show textual graph log."));
        mapSetting(graphButton, m_client->settings()->boolPointer(GitSettings::graphLogKey));
618
619
620
621
    }

    void executeCommand()
    {
622
        m_client->log(m_workingDirectory, m_fileNames, m_enableAnnotationContextMenu, arguments());
623
624
625
626
627
628
629
630
631
    }

private:
    GitClient *m_client;
    QString m_workingDirectory;
    bool m_enableAnnotationContextMenu;
    QStringList m_fileNames;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
632
633
634
635
class ConflictHandler : public QObject
{
    Q_OBJECT
public:
Orgad Shaneh's avatar
Orgad Shaneh committed
636
    ConflictHandler(VcsBase::Command *parentCommand,
Orgad Shaneh's avatar
Orgad Shaneh committed
637
638
                    const QString &workingDirectory,
                    const QString &command)
Orgad Shaneh's avatar
Orgad Shaneh committed
639
        : QObject(parentCommand),
Orgad Shaneh's avatar
Orgad Shaneh committed
640
641
642
          m_workingDirectory(workingDirectory),
          m_command(command)
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
643
644
645
646
        if (parentCommand) {
            connect(parentCommand, SIGNAL(outputData(QByteArray)), this, SLOT(readStdOut(QByteArray)));
            connect(parentCommand, SIGNAL(errorText(QString)), this, SLOT(readStdErr(QString)));
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
647
648
649
650
    }

    ~ConflictHandler()
    {
651
        GitClient *client = GitPlugin::instance()->gitClient();
Orgad Shaneh's avatar
Orgad Shaneh committed
652
        if (m_commit.isEmpty()) {
Orgad Shaneh's avatar
Orgad Shaneh committed
653
            GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(m_workingDirectory);
654
            if (client->checkCommandInProgress(m_workingDirectory) == GitClient::NoCommand)
655
                client->endStashScope(m_workingDirectory);
Orgad Shaneh's avatar
Orgad Shaneh committed
656
        } else {
657
            client->handleMergeConflicts(m_workingDirectory, m_commit, m_command);
Orgad Shaneh's avatar
Orgad Shaneh committed
658
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
    }

    void readStdOutString(const QString &data)
    {
        static QRegExp patchFailedRE(QLatin1String("Patch failed at ([^\\n]*)"));
        if (patchFailedRE.indexIn(data) != -1)
            m_commit = patchFailedRE.cap(1);
    }
public slots:
    void readStdOut(const QByteArray &data)
    {
        readStdOutString(QString::fromUtf8(data));
    }

    void readStdErr(const QString &data)
    {
        static QRegExp couldNotApplyRE(QLatin1String("[Cc]ould not (?:apply|revert) ([^\\n]*)"));
        if (couldNotApplyRE.indexIn(data) != -1)
            m_commit = couldNotApplyRE.cap(1);
    }
private:
    QString m_workingDirectory;
    QString m_command;
    QString m_commit;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
685

hjk's avatar
hjk committed
686
Core::IEditor *locateEditor(const char *property, const QString &entry)
con's avatar
con committed
687
{
hjk's avatar
hjk committed
688
    foreach (Core::IEditor *ed, Core::ICore::editorManager()->openedEditors())
689
        if (ed->document()->property(property).toString() == entry)
con's avatar
con committed
690
691
692
693
            return ed;
    return 0;
}

694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
// Return converted command output, remove '\r' read on Windows
static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
{
    QString output = QString::fromLocal8Bit(a);
    output.remove(QLatin1Char('\r'));
    return output;
}

// Return converted command output split into lines
static inline QStringList commandOutputLinesFromLocal8Bit(const QByteArray &a)
{
    QString output = commandOutputFromLocal8Bit(a);
    const QChar newLine = QLatin1Char('\n');
    if (output.endsWith(newLine))
        output.truncate(output.size() - 1);
    if (output.isEmpty())
        return QStringList();
    return output.split(newLine);
}

hjk's avatar
hjk committed
714
static inline VcsBase::VcsBaseOutputWindow *outputWindow()
715
{
hjk's avatar
hjk committed
716
    return VcsBase::VcsBaseOutputWindow::instance();
717
718
}

719
720
static inline QString msgRepositoryNotFound(const QString &dir)
{
Tobias Hunger's avatar
Tobias Hunger committed
721
    return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
722
723
724
725
}

static inline QString msgParseFilesFailed()
{
Tobias Hunger's avatar
Tobias Hunger committed
726
    return  GitClient::tr("Cannot parse the file output.");
727
728
}

729
730
static inline QString currentDocumentPath()
{
731
732
733
    if (Core::IEditor *editor = Core::EditorManager::currentEditor())
        return QFileInfo(editor->document()->fileName()).path();
    return QString();
734
735
}

736
// ---------------- GitClient
737
738
739

const char *GitClient::stashNamePrefix = "stash@{";

740
741
GitClient::GitClient(GitSettings *settings) :
    m_cachedGitVersion(0),
Tobias Hunger's avatar
Tobias Hunger committed
742
    m_msgWait(tr("Waiting for data...")),
743
    m_repositoryChangedSignalMapper(0),
744
745
    m_settings(settings),
    m_disableEditor(false)
con's avatar
con committed
746
{
747
    QTC_CHECK(settings);
hjk's avatar
hjk committed
748
    connect(Core::ICore::instance(), SIGNAL(saveSettingsRequested()), this, SLOT(saveSettings()));
749
750
751
    m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
            .arg(QCoreApplication::applicationFilePath())
            .arg(QCoreApplication::applicationPid());
con's avatar
con committed
752
753
754
755
756
757
}

GitClient::~GitClient()
{
}

758
const char *GitClient::noColorOption = "--no-color";
759
const char *GitClient::decorateOption = "--decorate";
760

con's avatar
con committed
761
762
QString GitClient::findRepositoryForDirectory(const QString &dir)
{
763
764
    if (dir.isEmpty() || dir.endsWith(QLatin1String("/.git"))
            || dir.contains(QLatin1String("/.git/"))) {
765
        return QString();
766
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
767
    QDir directory(dir);
768
769
770
    QString dotGit = QLatin1String(GIT_DIRECTORY);
    // QFileInfo is outside loop, because it is faster this way
    QFileInfo fileInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
771
    do {
772
773
774
775
776
777
778
        if (directory.exists(dotGit)) {
            fileInfo.setFile(directory, dotGit);
            if (fileInfo.isFile())
                return directory.absolutePath();
            else if (directory.exists(QLatin1String(".git/config")))
                return directory.absolutePath();
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
779
    } while (directory.cdUp());
780
    return QString();
con's avatar
con committed
781
782
}

Orgad Shaneh's avatar
Orgad Shaneh committed
783
QString GitClient::findGitDirForRepository(const QString &repositoryDir) const
Orgad Shaneh's avatar
Orgad Shaneh committed
784
{
785
786
787
788
    static QHash<QString, QString> repoDirCache;
    QString &res = repoDirCache[repositoryDir];
    if (!res.isEmpty())
        return res;
Orgad Shaneh's avatar
Orgad Shaneh committed
789
790
791
792
    QByteArray outputText;
    QStringList arguments;
    arguments << QLatin1String("rev-parse") << QLatin1String("--git-dir");
    fullySynchronousGit(repositoryDir, arguments, &outputText, 0, false);
793
    res = QString::fromLocal8Bit(outputText.trimmed());
794
795
796
    if (!QDir(res).isAbsolute())
        res.prepend(repositoryDir + QLatin1Char('/'));
    return res;
Orgad Shaneh's avatar
Orgad Shaneh committed
797
798
}

799
800
801
802
803
804
805
806
807
808
809
810
811
QString GitClient::findRepositoryForGitDir(const QString &gitDir) const
{
    static QHash<QString, QString> gitDirCache;
    QString &res = gitDirCache[gitDir];
    if (!res.isEmpty())
        return res;
    QByteArray outputText;
    QStringList arguments;
    arguments << QLatin1String("rev-parse") << QLatin1String("--show-toplevel");
    fullySynchronousGit(gitDir, arguments, &outputText, 0, false);
    return QString::fromLocal8Bit(outputText.trimmed());
}

hjk's avatar
hjk committed
812
VcsBase::VcsBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
Tobias Hunger's avatar
Tobias Hunger committed
813
                                                               const QString &dynamicPropertyValue) const
814
{
hjk's avatar
hjk committed
815
    VcsBase::VcsBaseEditorWidget *rc = 0;
hjk's avatar
hjk committed
816
    Core::IEditor *outputEditor = locateEditor(registerDynamicProperty, dynamicPropertyValue);
817
818
819
    if (!outputEditor)
        return 0;

820
    // Exists already
Eike Ziller's avatar
Eike Ziller committed
821
    Core::EditorManager::activateEditor(outputEditor);
822
    outputEditor->createNew(m_msgWait);
hjk's avatar
hjk committed
823
    rc = VcsBase::VcsBaseEditorWidget::getVcsBaseEditor(outputEditor);
824
825
826
827

    return rc;
}

hjk's avatar
hjk committed
828
829
DiffEditor::DiffEditor *GitClient::findExistingOrOpenNewDiffEditor(const char *registerDynamicProperty,
    const QString &dynamicPropertyValue, const QString &titlePattern) const
830
831
{
    Core::IEditor *outputEditor = locateEditor(registerDynamicProperty, dynamicPropertyValue);
hjk's avatar
hjk committed
832
833
    if (outputEditor) {
        // Exists already
Eike Ziller's avatar
Eike Ziller committed
834
        Core::EditorManager::activateEditor(outputEditor);
hjk's avatar
hjk committed
835
836
        outputEditor->createNew(m_msgWait);
    }
837

hjk's avatar
hjk committed
838
839
840
841
842
843
844
    DiffEditor::DiffEditor *editor = qobject_cast<DiffEditor::DiffEditor *>(outputEditor);
    if (!editor) {
        const Core::Id editorId = DiffEditor::Constants::DIFF_EDITOR_ID;
        QString title = titlePattern;
        editor = qobject_cast<DiffEditor::DiffEditor *>(
                    Core::EditorManager::openEditorWithContents(editorId, &title, m_msgWait));
        editor->document()->setProperty(registerDynamicProperty, dynamicPropertyValue);
Eike Ziller's avatar
Eike Ziller committed
845
        Core::EditorManager::activateEditor(editor); // should probably go outside this block
hjk's avatar
hjk committed
846
847
    }
    return editor;
848
849
}

hjk's avatar
hjk committed
850

con's avatar
con committed
851
852
853
854
/* 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). */
hjk's avatar
hjk committed
855
VcsBase::VcsBaseEditorWidget *GitClient::createVcsEditor(const Core::Id &id,
Tobias Hunger's avatar
Tobias Hunger committed
856
857
858
                                                         QString title,
                                                         // Source file or directory
                                                         const QString &source,
859
                                                         CodecType codecType,
Tobias Hunger's avatar
Tobias Hunger committed
860
861
862
863
                                                         // Dynamic property and value to identify that editor
                                                         const char *registerDynamicProperty,
                                                         const QString &dynamicPropertyValue,
                                                         QWidget *configWidget) const
con's avatar
con committed
864
{
hjk's avatar
hjk committed
865
    VcsBase::VcsBaseEditorWidget *rc = 0;
866
    QTC_CHECK(!findExistingVCSEditor(registerDynamicProperty, dynamicPropertyValue));
867
868

    // Create new, set wait message, set up with source and codec
hjk's avatar
hjk committed
869
    Core::IEditor *outputEditor = Core::EditorManager::openEditorWithContents(id, &title, m_msgWait);
870
    outputEditor->document()->setProperty(registerDynamicProperty, dynamicPropertyValue);
hjk's avatar
hjk committed
871
    rc = VcsBase::VcsBaseEditorWidget::getVcsBaseEditor(outputEditor);
872
873
874
875
    connect(rc, SIGNAL(annotateRevisionRequested(QString,QString,int)),
            this, SLOT(slotBlameRevisionRequested(QString,QString,int)));
    QTC_ASSERT(rc, return 0);
    rc->setSource(source);
876
    if (codecType == CodecSource) {
877
        rc->setCodec(getSourceCodec(source));
878
879
880
881
882
883
    } else if (codecType == CodecLogOutput) {
        QString encodingName = readConfigValue(source, QLatin1String("i18n.logOutputEncoding"));
        if (encodingName.isEmpty())
            encodingName = QLatin1String("utf-8");
        rc->setCodec(QTextCodec::codecForName(encodingName.toLocal8Bit()));
    }
884

885
    rc->setForceReadOnly(true);
Eike Ziller's avatar
Eike Ziller committed
886
    Core::EditorManager::activateEditor(outputEditor);
887
888
889
890

    if (configWidget)
        rc->setConfigurationWidget(configWidget);

con's avatar
con committed
891
892
893
    return rc;
}

894
895
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
896
897
                     const QStringList &unstagedFileNames,
                     const QStringList &stagedFileNames)
con's avatar
con committed
898
{
hjk's avatar
hjk committed
899
900
    const QString title = tr("Git Diff");
    const int timeout = settings()->intValue(GitSettings::timeoutKey);
901

hjk's avatar
hjk committed
902
903
    if (settings()->boolValue(GitSettings::useDiffEditorKey)) {
        DiffEditor::DiffEditor *editor = findExistingOrOpenNewDiffEditor("originalFileName", workingDirectory, title);
904

hjk's avatar
hjk committed
905
        GitDiffHandler *handler = new GitDiffHandler(editor, gitBinaryPath(), workingDirectory, processEnvironment(), timeout);
906
907
908
909

        if (unstagedFileNames.empty() && stagedFileNames.empty()) {
            // local repository diff
            handler->diffRepository();
jkobus's avatar
jkobus committed
910
911
912
        } else if (!stagedFileNames.empty()) {
            // diff of selected files only with --cached option, used in commit editor
            handler->diffFiles(stagedFileNames, unstagedFileNames);
913
        } else {
jkobus's avatar
jkobus committed
914
915
            // current project diff
            handler->diffProjects(unstagedFileNames);
916
        }
917
    } else {
918
919
920
921
922
923
924
925
926
927
928
929
        const QString binary = settings()->stringValue(GitSettings::binaryPathKey);
        const Core::Id editorId = Git::Constants::GIT_DIFF_EDITOR_ID;

        VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", workingDirectory);
        if (!editor) {
            GitCommitDiffArgumentsWidget *argWidget =
                    new GitCommitDiffArgumentsWidget(this, workingDirectory, diffArgs,
                                                     unstagedFileNames, stagedFileNames);

            editor = createVcsEditor(editorId, title,
                                     workingDirectory, CodecSource, "originalFileName", workingDirectory, argWidget);
            connect(editor, SIGNAL(diffChunkReverted(VcsBase::DiffChunk)), argWidget, SLOT(executeCommand()));
930
        }
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965

        GitCommitDiffArgumentsWidget *argWidget = qobject_cast<GitCommitDiffArgumentsWidget *>(editor->configurationWidget());
        QStringList userDiffArgs = argWidget->arguments();
        editor->setDiffBaseDirectory(workingDirectory);

        // 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.
        VcsBase::Command *command = createCommand(workingDirectory, editor);
        // Directory diff?

        QStringList cmdArgs;
        cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption);

        if (unstagedFileNames.empty() && stagedFileNames.empty()) {
            QStringList arguments(cmdArgs);
            arguments << userDiffArgs;
            outputWindow()->appendCommand(workingDirectory, binary, arguments);
            command->addJob(arguments, timeout);
        } else {
            // Files diff.
            if (!unstagedFileNames.empty()) {
                QStringList arguments(cmdArgs);
                arguments << userDiffArgs;
                arguments << QLatin1String("--") << unstagedFileNames;
                outputWindow()->appendCommand(workingDirectory, binary, arguments);
                command->addJob(arguments, timeout);
            }
            if (!stagedFileNames.empty()) {
                QStringList arguments(cmdArgs);
                arguments << userDiffArgs;
                arguments << QLatin1String("--cached") << diffArgs << QLatin1String("--") << stagedFileNames;
                outputWindow()->appendCommand(workingDirectory, binary, arguments);
                command->addJob(arguments, timeout);
            }
966
        }
967
        command->execute();
968
    }
con's avatar
con committed
969
970
}

971
972
973
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
                     const QString &fileName)
con's avatar
con committed
974
{
hjk's avatar
hjk committed
975
    const QString title = tr("Git Diff \"%1\"").arg(fileName);
976
977
    if (settings()->boolValue(GitSettings::useDiffEditorKey)) {
        const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, fileName);
hjk's avatar
hjk committed
978
        DiffEditor::DiffEditor *editor = findExistingOrOpenNewDiffEditor("originalFileName", sourceFile, title);
979
980
981

        if (!fileName.isEmpty()) {
            int timeout = settings()->intValue(GitSettings::timeoutKey);
hjk's avatar
hjk committed
982
            GitDiffHandler *handler = new GitDiffHandler(editor, gitBinaryPath(), workingDirectory, processEnvironment(), timeout);
983
984
985
986
987
            handler->diffFile(fileName);
        }
    } else {
        const Core::Id editorId = Git::Constants::GIT_DIFF_EDITOR_ID;
        const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, fileName);
988

989
990
991
992
        VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", sourceFile);
        if (!editor) {
            GitFileDiffArgumentsWidget *argWidget =
                    new GitFileDiffArgumentsWidget(this, workingDirectory, diffArgs, fileName);
993

994
995
996
997
            editor = createVcsEditor(editorId, title, sourceFile, CodecSource, "originalFileName", sourceFile, argWidget);
            connect(editor, SIGNAL(diffChunkReverted(VcsBase::DiffChunk)), argWidget, SLOT(executeCommand()));
        }
        editor->setDiffBaseDirectory(workingDirectory);
998

999
1000
        GitFileDiffArgumentsWidget *argWidget = qobject_cast<GitFileDiffArgumentsWidget *>(editor->configurationWidget());
        QStringList userDiffArgs = argWidget->arguments();
1001

1002
1003
1004
        QStringList cmdArgs;
        cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
                << userDiffArgs;
1005

1006
1007
1008
1009
        if (!fileName.isEmpty())
            cmdArgs << QLatin1String("--") << fileName;
        executeGit(workingDirectory, cmdArgs, editor);
    }
con's avatar
con committed
1010
1011
}

1012
1013
1014
1015
void GitClient::diffBranch(const QString &workingDirectory,
                           const QStringList &diffArgs,
                           const QString &branchName)
{
hjk's avatar
hjk committed
1016
    const QString title = tr("Git Diff Branch \"%1\"").arg(branchName);
1017
    if (settings()->boolValue(GitSettings::useDiffEditorKey)) {
hjk's avatar
hjk committed
1018
1019
        DiffEditor::DiffEditor *editor =
                findExistingOrOpenNewDiffEditor("BranchName", branchName, title);
1020

1021
        int timeout = settings()->intValue(GitSettings::timeoutKey);
hjk's avatar
hjk committed
1022
        GitDiffHandler *handler = new GitDiffHandler(editor, gitBinaryPath(), workingDirectory, processEnvironment(), timeout);
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
        handler->diffBranch(branchName);
    } else {
        const Core::Id editorId = Git::Constants::GIT_DIFF_EDITOR_ID;
        const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, QStringList());

        VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("BranchName", branchName);
        if (!editor)
            editor = createVcsEditor(editorId, title, sourceFile, CodecSource, "BranchName", branchName,
                                     new GitBranchDiffArgumentsWidget(this, workingDirectory,
                                                                      diffArgs, branchName));
        editor->setDiffBaseDirectory(workingDirectory);
1034

1035
1036
1037
1038
1039
1040
        GitBranchDiffArgumentsWidget *argWidget = qobject_cast<GitBranchDiffArgumentsWidget *>(editor->configurationWidget());
        QStringList userDiffArgs = argWidget->arguments();

        QStringList cmdArgs;
        cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
                  << userDiffArgs  << branchName;
1041

1042
1043
        executeGit(workingDirectory, cmdArgs, editor);
    }
1044
1045
}

1046
1047
1048
1049
1050
1051
1052
void GitClient::merge(const QString &workingDirectory, const QStringList &unmergedFileNames)
{
    MergeTool *mergeTool = new MergeTool(this);
    if (!mergeTool->start(workingDirectory, unmergedFileNames))
        delete mergeTool;
}

con's avatar
con committed
1053
1054
void GitClient::status(const QString &workingDirectory)
{
1055
    // @TODO: Use "--no-color" once it is supported
1056
1057
    QStringList statusArgs(QLatin1String("status"));
    statusArgs << QLatin1String("-u");
hjk's avatar
hjk committed