gitclient.cpp 153 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3
** Copyright (C) 2014 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"
39
#include "branchadddialog.h"
Christian Kandeler's avatar
Christian Kandeler committed
40
#include "gerrit/gerritplugin.h"
con's avatar
con committed
41

42
43
#include <vcsbase/submitfilemodel.h>

con's avatar
con committed
44
#include <coreplugin/editormanager/editormanager.h>
hjk's avatar
hjk committed
45
#include <coreplugin/icore.h>
46
#include <coreplugin/vcsmanager.h>
47
#include <coreplugin/id.h>
48
#include <coreplugin/iversioncontrol.h>
49
#include <coreplugin/coreconstants.h>
50

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

62
#include <diffeditor/diffeditorconstants.h>
jkobus's avatar
jkobus committed
63
64
65
#include <diffeditor/diffeditorcontroller.h>
#include <diffeditor/diffeditordocument.h>
#include <diffeditor/diffeditormanager.h>
66

67
#include <QCoreApplication>
68
#include <QDir>
69
#include <QFileInfo>
70
#include <QHash>
71
#include <QRegExp>
72
#include <QSignalMapper>
73
#include <QTime>
con's avatar
con committed
74

75
#include <QMessageBox>
76
#include <QPushButton>
77
78
#include <QToolButton>
#include <QTextCodec>
con's avatar
con committed
79

80
static const char GIT_DIRECTORY[] = ".git";
81
static const char graphLogFormatC[] = "%h %d %an %s %ci";
82
static const char HEAD[] = "HEAD";
83
84
static const char noColorOption[] = "--no-color";
static const char decorateOption[] = "--decorate";
con's avatar
con committed
85

86
87
88
namespace Git {
namespace Internal {

89
90
91
92
93
94
// Suppress git diff warnings about "LF will be replaced by CRLF..." on Windows.
static inline unsigned diffExecutionFlags()
{
    return Utils::HostOsInfo::isWindowsHost() ? unsigned(VcsBase::VcsBasePlugin::SuppressStdErrInLogWindow) : 0u;
}

95
96
using VcsBase::VcsBasePlugin;

97
98
99
100
101
102
103
104
105
106
107
108
109
110
class GitDiffSwitcher : public QObject
{
    Q_OBJECT

public:
    enum DiffType {
        DiffRepository,
        DiffFile,
        DiffFileList,
        DiffProjectList,
        DiffBranch,
        DiffShow
    };

jkobus's avatar
jkobus committed
111
    GitDiffSwitcher(Core::IDocument *parentDocument, GitClient *gitClient);
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

    void setWorkingDirectory(const QString &workingDir) { m_workingDirectory = workingDir; }
    void setDiffType(DiffType type) { m_diffType = type; }
    void setFileName(const QString &fileName) { m_fileName = fileName; }
    void setFileList(const QStringList &stagedFiles, const QStringList &unstagedFiles)
    {
        m_stagedFiles = stagedFiles;
        m_unstagedFiles = unstagedFiles;
    }
    void setProjectList(const QStringList &projectFiles) { m_projectFiles = projectFiles; }
    void setBranchName(const QString &branchName) { m_branchName = branchName; }
    void setId(const QString &id) { m_id = id; }
    void setDisplayName(const QString &displayName) { m_displayName = displayName; }
    void setBaseArguments(const QStringList &args) { m_baseArguments = args; }

jkobus's avatar
jkobus committed
127
128
129
130
private slots:
    void slotEditorOpened(Core::IEditor *editor);
    void slotEditorClosed(Core::IEditor *editor);
    void execute(QObject *editor);
131
132

private:
jkobus's avatar
jkobus committed
133
134
135
136
    void attachAction(Core::IEditor *editor);
    QString actionText();

    Core::IDocument *m_document;
137
138
139
    GitClient *m_gitClient;
    QString m_workingDirectory;
    DiffType m_diffType;
140
    bool m_usingDiffEditor;
141
142
143
144
145
146
147
148
    QString m_fileName;
    QStringList m_stagedFiles;
    QStringList m_unstagedFiles;
    QStringList m_projectFiles;
    QString m_branchName;
    QString m_id;
    QString m_displayName;
    QStringList m_baseArguments;
jkobus's avatar
jkobus committed
149
150
151

    QSignalMapper *m_signalMapper;
    QMap<QObject *, bool> m_editorToUsingSideBySideDiffEditor;
152
153
};

jkobus's avatar
jkobus committed
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
GitDiffSwitcher::GitDiffSwitcher(Core::IDocument *parentDocument, GitClient *gitClient)
    : QObject(parentDocument),
      m_document(parentDocument),
      m_gitClient(gitClient),
      m_signalMapper(new QSignalMapper(this))
{
    Core::DocumentModel *documentModel = Core::EditorManager::documentModel();
    QList<Core::IEditor *> editors = documentModel->editorsForDocument(m_document);
    for (int i = 0; i < editors.count(); i++)
        attachAction(editors.at(i));

    // must be queued connection because execute() removes the editor & tool bar that the action was added to
    connect(m_signalMapper, SIGNAL(mapped(QObject*)), this, SLOT(execute(QObject*)), Qt::QueuedConnection);
    connect(Core::EditorManager::instance(), SIGNAL(editorOpened(Core::IEditor*)),
            this, SLOT(slotEditorOpened(Core::IEditor*)));
    connect(Core::EditorManager::instance(), SIGNAL(editorAboutToClose(Core::IEditor*)),
            this, SLOT(slotEditorClosed(Core::IEditor*)));
}

void GitDiffSwitcher::attachAction(Core::IEditor *editor)
174
{
jkobus's avatar
jkobus committed
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
    if (m_editorToUsingSideBySideDiffEditor.contains(editor))
        return;

    const bool usingSideBySideDiffEditor = m_gitClient->settings()->boolValue(GitSettings::useDiffEditorKey);
    QIcon actionIcon = usingSideBySideDiffEditor
            ? QIcon(QLatin1String(Core::Constants::ICON_TEXT_DIFF))
            : QIcon(QLatin1String(Core::Constants::ICON_SIDE_BY_SIDE_DIFF));

    const QString actionToolTip = usingSideBySideDiffEditor
            ? tr("Switch to Text Diff Editor")
            : tr("Switch to Side By Side Diff Editor");

    QAction *switchAction = new QAction(actionIcon, actionToolTip, editor);
    editor->toolBar()->addAction(switchAction);
    connect(switchAction, SIGNAL(triggered()),
            m_signalMapper, SLOT(map()));
    m_signalMapper->setMapping(switchAction, editor);

    m_editorToUsingSideBySideDiffEditor.insert(editor, usingSideBySideDiffEditor);
}

void GitDiffSwitcher::slotEditorOpened(Core::IEditor *editor)
{
    Core::IDocument *document = editor->document();
    if (document != m_document)
        return;

    attachAction(editor);
}

void GitDiffSwitcher::slotEditorClosed(Core::IEditor *editor)
{
    Core::IDocument *document = editor->document();
    if (document != m_document)
        return;

    m_editorToUsingSideBySideDiffEditor.remove(editor);
}

void GitDiffSwitcher::execute(QObject *editor)
{
    bool usingSideBySideEditor = !m_editorToUsingSideBySideDiffEditor.value(editor);
    m_editorToUsingSideBySideDiffEditor[editor] = usingSideBySideEditor;
    m_gitClient->settings()->setValue(GitSettings::useDiffEditorKey, usingSideBySideEditor);

    Core::IEditor *ieditor = qobject_cast<Core::IEditor*>(editor);
    Core::EditorManager::activateEditor(ieditor);

223
224
    switch (m_diffType) {
    case DiffRepository:
225
        m_gitClient->diff(m_workingDirectory, QStringList(), QStringList());
226
227
        break;
    case DiffFile:
228
        m_gitClient->diff(m_workingDirectory, m_fileName);
229
230
        break;
    case DiffFileList:
231
        m_gitClient->diff(m_workingDirectory, m_unstagedFiles, m_stagedFiles);
232
233
        break;
    case DiffProjectList:
234
        m_gitClient->diff(m_workingDirectory, m_projectFiles, QStringList());
235
236
        break;
    case DiffBranch:
237
        m_gitClient->diffBranch(m_workingDirectory, m_baseArguments, m_branchName);
238
239
        break;
    case DiffShow:
240
        m_gitClient->show(m_fileName, m_id, m_baseArguments, m_displayName);
241
242
243
244
        break;
    default:
        break;
    }
jkobus's avatar
jkobus committed
245
246

    Core::EditorManager::closeEditor(ieditor, false);
247
248
}

249
250
251
252
253
class GitDiffHandler : public QObject
{
    Q_OBJECT

public:
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
    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;
            }
        }
    };

jkobus's avatar
jkobus committed
276
    GitDiffHandler(DiffEditor::DiffEditorController *editorController,
jkobus's avatar
jkobus committed
277
                   const QString &gitPath,
278
279
280
281
282
283
                   const QString &workingDirectory,
                   const QProcessEnvironment &environment,
                   int timeout);

    // index -> working tree
    void diffFile(const QString &fileName);
284
285
    // stagedFileNames:   HEAD -> index
    // unstagedFileNames: index -> working tree
286
287
288
289
290
    void diffFiles(const QStringList &stagedFileNames, const QStringList &unstagedFileNames);
    // index -> working tree
    void diffProjects(const QStringList &projectPaths);
    // index -> working tree
    void diffRepository();
291
292
293
294
    // branch HEAD -> working tree
    void diffBranch(const QString &branchName);
    // id^ -> id
    void show(const QString &id);
295
296

private slots:
Orgad Shaneh's avatar
Orgad Shaneh committed
297
298
299
    void slotShowDescriptionReceived(const QString &data);
    void slotFileListReceived(const QString &fileList);
    void slotFileContentsReceived(const QString &contents);
300
301

private:
302
    void collectShowDescription(const QString &id);
303
    void collectFilesList(const QStringList &additionalArguments);
304
    void prepareForCollection();
305
306
307
308
    void collectFilesContents();
    void feedEditor();
    QString workingTreeContents(const QString &fileName) const;

jkobus's avatar
jkobus committed
309
    QPointer<DiffEditor::DiffEditorController> m_editorController;
310
311
312
313
314
315
    const QString m_gitPath;
    const QString m_workingDirectory;
    const QProcessEnvironment m_processEnvironment;
    const int m_timeout;
    const QString m_waitMessage;

316
317
318
319
320
    struct RevisionRange {
        RevisionRange() { }
        RevisionRange(const Revision &b, const Revision &e) : begin(b), end(e) { }
        Revision begin;
        Revision end;
321
322
    };

323
324
325
326
327
328
    // 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;
329

330
    RevisionRange m_requestedRevisionRange;
331
332
};

333
334
335
336
337
338
339
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;
}

jkobus's avatar
jkobus committed
340
GitDiffHandler::GitDiffHandler(DiffEditor::DiffEditorController *editorController,
jkobus's avatar
jkobus committed
341
               const QString &gitPath,
342
343
344
               const QString &workingDirectory,
               const QProcessEnvironment &environment,
               int timeout)
jkobus's avatar
jkobus committed
345
    : m_editorController(editorController),
jkobus's avatar
jkobus committed
346
      m_gitPath(gitPath),
347
348
349
350
351
352
353
354
355
      m_workingDirectory(workingDirectory),
      m_processEnvironment(environment),
      m_timeout(timeout),
      m_waitMessage(tr("Waiting for data..."))
{
}

void GitDiffHandler::diffFile(const QString &fileName)
{
356
    m_requestedRevisionRange = RevisionRange(
357
                Revision(Index),
358
359
360
                Revision(WorkingTree));

    collectFilesList(QStringList() << QLatin1String("--") << fileName);
361
362
363
364
}

void GitDiffHandler::diffFiles(const QStringList &stagedFileNames, const QStringList &unstagedFileNames)
{
365
366
    RevisionRange stagedRange = RevisionRange(
                Revision(Other, QLatin1String(HEAD)),
367
                Revision(Index));
368
369
370
    RevisionRange unstagedRange = RevisionRange(
                Revision(Index),
                Revision(WorkingTree));
371

372
373
374
375
376
377
378
    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();
379
380
381
382
383
    collectFilesContents();
}

void GitDiffHandler::diffProjects(const QStringList &projectPaths)
{
384
    m_requestedRevisionRange = RevisionRange(
385
                Revision(Index),
386
387
                Revision(WorkingTree));

388
389
390
391
392
    collectFilesList(QStringList() << QLatin1String("--") << projectPaths);
}

void GitDiffHandler::diffRepository()
{
393
    m_requestedRevisionRange = RevisionRange(
394
                Revision(Index),
395
396
                Revision(WorkingTree));

397
398
399
    collectFilesList(QStringList());
}

400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
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);

415
416
417
418
419
    collectShowDescription(id);
}

void GitDiffHandler::collectShowDescription(const QString &id)
{
jkobus's avatar
jkobus committed
420
421
    if (m_editorController.isNull()) {
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
422
        return;
jkobus's avatar
jkobus committed
423
424
425
    }

    m_editorController->clear(m_waitMessage);
426
    VcsBase::Command *command = new VcsBase::Command(m_gitPath, m_workingDirectory, m_processEnvironment);
427
428
    command->setCodec(GitPlugin::instance()->gitClient()->encoding(m_workingDirectory,
                                                                   "i18n.commitEncoding"));
429
    connect(command, SIGNAL(output(QString)), this, SLOT(slotShowDescriptionReceived(QString)));
430
    QStringList arguments;
431
    arguments << QLatin1String("show") << QLatin1String("-s")
432
              << QLatin1String(noColorOption) << QLatin1String(decorateOption) << id;
433
434
435
436
    command->addJob(arguments, m_timeout);
    command->execute();
}

Orgad Shaneh's avatar
Orgad Shaneh committed
437
void GitDiffHandler::slotShowDescriptionReceived(const QString &description)
438
{
jkobus's avatar
jkobus committed
439
440
    if (m_editorController.isNull()) {
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
441
        return;
jkobus's avatar
jkobus committed
442
    }
Jarek Kobus's avatar
Jarek Kobus committed
443

jkobus's avatar
jkobus committed
444
445
    m_editorController->setDescription(GitPlugin::instance()->gitClient()->
                                           extendedShowDescription(m_workingDirectory, description));
446
447
448
449

    collectFilesList(QStringList()
                     << m_requestedRevisionRange.begin.id
                     << m_requestedRevisionRange.end.id);
450
451
}

452
453
void GitDiffHandler::collectFilesList(const QStringList &additionalArguments)
{
jkobus's avatar
jkobus committed
454
455
    if (m_editorController.isNull()) {
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
456
        return;
jkobus's avatar
jkobus committed
457
458
459
    }

    m_editorController->clear(m_waitMessage);
460
    VcsBase::Command *command = new VcsBase::Command(m_gitPath, m_workingDirectory, m_processEnvironment);
jkobus's avatar
jkobus committed
461
    command->setCodec(Core::EditorManager::defaultTextCodec());
462
    connect(command, SIGNAL(output(QString)), this, SLOT(slotFileListReceived(QString)));
463
464
465
    QStringList arguments;
    arguments << QLatin1String("diff") << QLatin1String("--name-only") << additionalArguments;
    command->addJob(arguments, m_timeout);
466
    command->addFlags(diffExecutionFlags());
467
468
469
    command->execute();
}

Orgad Shaneh's avatar
Orgad Shaneh committed
470
void GitDiffHandler::slotFileListReceived(const QString &fileList)
471
{
jkobus's avatar
jkobus committed
472
473
    if (m_editorController.isNull()) {
        deleteLater();
474
        return;
jkobus's avatar
jkobus committed
475
    }
476

477
478
479
480
481
    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);
482

483
    prepareForCollection();
484
485
486
    collectFilesContents();
}

487
void GitDiffHandler::prepareForCollection()
488
{
489
490
491
492
493
494
495
496
497
498
499
500
    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;
        }
501

502
503
504
        ++it;
    }
}
505

506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
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);
jkobus's avatar
jkobus committed
531
                command->setCodec(Core::EditorManager::defaultTextCodec());
532
                connect(command, SIGNAL(output(QString)), this, SLOT(slotFileContentsReceived(QString)));
533
534
535

                QString revisionArgument = (revision.type == Other)
                        ? revision.id : QString();
536
                revisionArgument += QLatin1Char(':');
537
538
539
540
541
542
543
544
                QStringList arguments;
                arguments << QLatin1String("show") << revisionArgument + fileName;
                command->addJob(arguments, m_timeout);
                command->execute();

                return;
            }
        }
545

546
        itFile = m_pendingRevisions.erase(itFile); // iterate to the next file
547
    }
548
549

    feedEditor();
550
551
}

Orgad Shaneh's avatar
Orgad Shaneh committed
552
void GitDiffHandler::slotFileContentsReceived(const QString &contents)
553
{
jkobus's avatar
jkobus committed
554
555
    if (m_editorController.isNull()) {
        deleteLater();
556
        return;
jkobus's avatar
jkobus committed
557
    }
558

559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
    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) {
            m_collectedRevisions[fileName][itRev.key()] = contents;

            itRev = revisions.erase(itRev);
            if (revisions.isEmpty())
                m_pendingRevisions.erase(itFile);
        }
    }
578
579
580
581
582
583

    collectFilesContents();
}

void GitDiffHandler::feedEditor()
{
jkobus's avatar
jkobus committed
584
585
586
587
588
    if (m_editorController.isNull()) {
        deleteLater();
        return;
    }

jkobus's avatar
jkobus committed
589
    QList<DiffEditor::DiffEditorController::DiffFilesContents> list;
590

591
592
593
594
595
596
597
598
599
600
601
    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;

jkobus's avatar
jkobus committed
602
603
            DiffEditor::DiffEditorController::DiffFilesContents dfc;
            dfc.leftFileInfo = DiffEditor::DiffEditorController::DiffFileInfo(fileName, leftRevision.infoText());
604
            dfc.leftText = m_collectedRevisions[fileName][leftRevision];
jkobus's avatar
jkobus committed
605
            dfc.rightFileInfo = DiffEditor::DiffEditorController::DiffFileInfo(fileName, rightRevision.infoText());
606
            dfc.rightText = m_collectedRevisions[fileName][rightRevision];
607
608
            list.append(dfc);
        }
609
610

        ++itFile;
611
    }
612

jkobus's avatar
jkobus committed
613
    m_editorController->setDiffContents(list, m_workingDirectory);
614
615
616
617
618
619
620
621
622
    deleteLater();
}

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

    QFile file(absoluteFileName);
Orgad Shaneh's avatar
Orgad Shaneh committed
623
    if (file.open(QIODevice::ReadOnly | QIODevice::Text))
jkobus's avatar
jkobus committed
624
        return Core::EditorManager::defaultTextCodec()->toUnicode(file.readAll());
625
626
627
628
629
    return QString();
}

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

hjk's avatar
hjk committed
630
class BaseGitDiffArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
631
{
Friedemann Kleint's avatar
Friedemann Kleint committed
632
    Q_OBJECT
633

634
public:
635
    BaseGitDiffArgumentsWidget(GitClient *client, const QString &directory,
636
                               const QStringList &args) :
637
        m_workingDirectory(directory),
638
        m_client(client)
639
    {
640
641
        QTC_ASSERT(!directory.isEmpty(), return);
        QTC_ASSERT(m_client, return);
642

643
644
645
        m_patienceButton = addToggleButton(QLatin1String("--patience"), tr("Patience"),
                                           tr("Use the patience algorithm for calculating the differences."));
        mapSetting(m_patienceButton, client->settings()->boolPointer(GitSettings::diffPatienceKey));
646
        m_ignoreWSButton = addToggleButton(QLatin1String("--ignore-space-change"), tr("Ignore Whitespace"),
647
648
                                           tr("Ignore whitespace only changes."));
        mapSetting(m_ignoreWSButton, m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInDiffKey));
649
650

        setBaseArguments(args);
651
652
    }

653
protected:
654
655
    QString m_workingDirectory;
    GitClient *m_client;
656
657
    QToolButton *m_patienceButton;
    QToolButton *m_ignoreWSButton;
658
659
660
661
};

class GitCommitDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
662
    Q_OBJECT
663

664
public:
665
    GitCommitDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
Orgad Shaneh's avatar
Orgad Shaneh committed
666
                                 const QStringList &unstaged, const QStringList &staged) :
667
668
669
670
671
672
673
674
675
676
        BaseGitDiffArgumentsWidget(client, directory, QStringList())
    {
        setFileNames(unstaged, staged);
    }

    void setFileNames(const QStringList &unstaged, const QStringList &staged)
    {
        m_unstagedFileNames = unstaged;
        m_stagedFileNames = staged;
    }
677

678
    void executeCommand()
679
    {
680
        m_client->diff(m_workingDirectory, m_unstagedFileNames, m_stagedFileNames);
681
682
683
    }

private:
684
685
    QStringList m_unstagedFileNames;
    QStringList m_stagedFileNames;
686
687
688
689
};

class GitFileDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
690
    Q_OBJECT
691
public:
692
    GitFileDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
Orgad Shaneh's avatar
Orgad Shaneh committed
693
694
                               const QString &file) :
        BaseGitDiffArgumentsWidget(client, directory, QStringList()),
695
696
697
        m_fileName(file)
    { }

698
    void executeCommand()
699
    {
700
        m_client->diff(m_workingDirectory, m_fileName);
701
702
703
704
705
706
707
708
    }

private:
    const QString m_fileName;
};

class GitBranchDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
709
    Q_OBJECT
710
public:
711
    GitBranchDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
712
                                 const QStringList &args, const QString &branch) :
713
        BaseGitDiffArgumentsWidget(client, directory, args),
714
715
716
        m_branchName(branch)
    { }

717
    void executeCommand()
718
    {
719
        m_client->diffBranch(m_workingDirectory, baseArguments(), m_branchName);
720
721
722
723
724
725
    }

private:
    const QString m_branchName;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
726
class GitShowArgumentsWidget : public BaseGitDiffArgumentsWidget
727
{
Friedemann Kleint's avatar
Friedemann Kleint committed
728
    Q_OBJECT
729

730
public:
731
    GitShowArgumentsWidget(Git::Internal::GitClient *client,
732
733
734
                           const QString &directory,
                           const QStringList &args,
                           const QString &id) :
Orgad Shaneh's avatar
Orgad Shaneh committed
735
        BaseGitDiffArgumentsWidget(client, directory, args),
736
737
        m_client(client),
        m_workingDirectory(directory),
738
739
        m_id(id)
    {
740
741
742
743
744
745
746
747
        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"));
748
        mapSetting(addComboBox(QStringList(QLatin1String("--pretty=%1")), prettyChoices),
749
                   m_client->settings()->intPointer(GitSettings::showPrettyFormatKey));
750
751
752
    }

    void executeCommand()
753
    {
754
        m_client->show(m_workingDirectory, m_id, baseArguments());
755
756
757
    }

private:
758
759
    GitClient *m_client;
    QString m_workingDirectory;
760
    QString m_id;
761
762
};

hjk's avatar
hjk committed
763
class GitBlameArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
764
{
Friedemann Kleint's avatar
Friedemann Kleint committed
765
    Q_OBJECT
766

767
public:
768
769
770
771
    GitBlameArgumentsWidget(Git::Internal::GitClient *client,
                            const QString &directory,
                            const QStringList &args,
                            const QString &revision, const QString &fileName) :
772
        m_editor(0),
773
774
        m_client(client),
        m_workingDirectory(directory),
775
776
777
        m_revision(revision),
        m_fileName(fileName)
    {
778
779
780
        mapSetting(addToggleButton(QString(), tr("Omit Date"),
                                   tr("Hide the date of a change from the output.")),
                   m_client->settings()->boolPointer(GitSettings::omitAnnotationDateKey));
781
        mapSetting(addToggleButton(QLatin1String("-w"), tr("Ignore Whitespace"),
782
783
                                   tr("Ignore whitespace only changes.")),
                   m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
784
785

        setBaseArguments(args);
786
787
    }

hjk's avatar
hjk committed
788
    void setEditor(VcsBase::VcsBaseEditorWidget *editor)
789
    {
790
        QTC_ASSERT(editor, return);
791
792
793
        m_editor = editor;
    }

794
    void executeCommand()
795
    {
796
797
798
        int line = -1;
        if (m_editor)
            line = m_editor->lineNumberOfCurrentEditor();
799
        m_client->blame(m_workingDirectory, baseArguments(), m_fileName, m_revision, line);
800
801
802
    }

private:
hjk's avatar
hjk committed
803
    VcsBase::VcsBaseEditorWidget *m_editor;
804
805
    GitClient *m_client;
    QString m_workingDirectory;
806
807
808
809
    QString m_revision;
    QString m_fileName;
};

810
811
812
813
814
815
816
817
818
class GitLogArgumentsWidget : public BaseGitDiffArgumentsWidget
{
    Q_OBJECT

public:
    GitLogArgumentsWidget(Git::Internal::GitClient *client,
                          const QString &directory,
                          bool enableAnnotationContextMenu,
                          const QStringList &args,
819
                          const QString &fileName) :
820
821
822
        BaseGitDiffArgumentsWidget(client, directory, args),
        m_client(client),
        m_workingDirectory(directory),
823
        m_enableAnnotationContextMenu(enableAnnotationContextMenu)
824
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
825
        QTC_ASSERT(!directory.isEmpty(), return);
826
        QToolButton *diffButton = addToggleButton(QLatin1String("--patch"), tr("Show Diff"),
827
                                              tr("Show difference."));
828
829
830
831
832
833
834
835
836
837
838
        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));
839
        setFileName(fileName);
840
841
    }

842
    void setFileName(const QString &fileNames)
843
    {
844
        m_fileName = fileNames;
845
846
847
848
    }

    void executeCommand()
    {
849
        m_client->log(m_workingDirectory, m_fileName, m_enableAnnotationContextMenu, baseArguments());
850
851
852
853
854
855
    }

private:
    GitClient *m_client;
    QString m_workingDirectory;
    bool m_enableAnnotationContextMenu;
856
    QString m_fileName;
857
858
};

Orgad Shaneh's avatar
Orgad Shaneh committed
859
860
861
862
class ConflictHandler : public QObject
{
    Q_OBJECT
public:
Orgad Shaneh's avatar
Orgad Shaneh committed
863
    ConflictHandler(VcsBase::Command *parentCommand,
Orgad Shaneh's avatar
Orgad Shaneh committed
864
                    const QString &workingDirectory,
865
                    const QString &command = QString())
Orgad Shaneh's avatar
Orgad Shaneh committed
866
        : QObject(parentCommand),
Orgad Shaneh's avatar
Orgad Shaneh committed
867
868
869
          m_workingDirectory(workingDirectory),
          m_command(command)
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
870
        if (parentCommand) {
Orgad Shaneh's avatar
Orgad Shaneh committed
871
            parentCommand->addFlags(VcsBasePlugin::ExpectRepoChanges);
872
            connect(parentCommand, SIGNAL(output(QString)), this, SLOT(readStdOut(QString)));
Orgad Shaneh's avatar
Orgad Shaneh committed
873
874
            connect(parentCommand, SIGNAL(errorText(QString)), this, SLOT(readStdErr(QString)));
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
875
876
877
878
    }

    ~ConflictHandler()
    {
879
880
881
882
883
884
885
886
887
888
        // If interactive rebase editor window is closed, plugin is terminated
        // but referenced here when the command ends
        if (GitPlugin *plugin = GitPlugin::instance()) {
            GitClient *client = plugin->gitClient();
            if (m_commit.isEmpty() && m_files.isEmpty()) {
                if (client->checkCommandInProgress(m_workingDirectory) == GitClient::NoCommand)
                    client->endStashScope(m_workingDirectory);
            } else {
                client->handleMergeConflicts(m_workingDirectory, m_commit, m_files, m_command);
            }
Orgad Shaneh's avatar
Orgad Shaneh committed
889
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
890
891
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
892
893
public slots:
    void readStdOut(const QString &data)
Orgad Shaneh's avatar
Orgad Shaneh committed
894
895
    {
        static QRegExp patchFailedRE(QLatin1String("Patch failed at ([^\\n]*)"));
896
        static QRegExp conflictedFilesRE(QLatin1String("Merge conflict in ([^\\n]*)"));
Orgad Shaneh's avatar
Orgad Shaneh committed
897
898
        if (patchFailedRE.indexIn(data) != -1)
            m_commit = patchFailedRE.cap(1);
899
900
901
902
        int fileIndex = -1;
        while ((fileIndex = conflictedFilesRE.indexIn(data, fileIndex + 1)) != -1) {
            m_files.append(conflictedFilesRE.cap(1));
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
903
904
905
906
907
908
909
910
911
912
913
914
    }

    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;
915
    QStringList m_files;
Orgad Shaneh's avatar
Orgad Shaneh committed
916
917
};

918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
class ProgressParser : public VcsBase::ProgressParser
{
public:
    ProgressParser() :
        m_progressExp(QLatin1String("\\((\\d+)/(\\d+)\\)")) // e.g. Rebasing (7/42)
    {
    }

protected:
    void parseProgress(const QString &text)
    {
        if (m_progressExp.lastIndexIn(text) != -1)
            setProgressAndMaximum(m_progressExp.cap(1).toInt(), m_progressExp.cap(2).toInt());
    }

private:
    QRegExp m_progressExp;
};


Orgad Shaneh's avatar
Orgad Shaneh committed
938

hjk's avatar
hjk committed
939
Core::IEditor *locateEditor(const char *property, const QString &entry)
con's avatar
con committed
940
{
941
942
943
    foreach (Core::IDocument *document, Core::EditorManager::documentModel()->openedDocuments())
        if (document->property(property).toString() == entry)
            return Core::EditorManager::documentModel()->editorsForDocument(document).first();
con's avatar
con committed
944
945
946
    return 0;
}

947
948
949
// Return converted command output, remove '\r' read on Windows
static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
{
950
    return Utils::SynchronousProcess::normalizeNewlines(QString::fromLocal8Bit(a));
951
952
953
954
955
956
957
958
959
960
961
962
963
964
}

// 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
965
static inline VcsBase::VcsBaseOutputWindow *outputWindow()
966
{
hjk's avatar
hjk committed
967
    return VcsBase::VcsBaseOutputWindow::instance();
968
969
}

970
971
static inline QString msgRepositoryNotFound(const QString &dir)
{
Tobias Hunger's avatar
Tobias Hunger committed
972
    return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
973
974
975
976
}

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

Friedemann Kleint's avatar
Friedemann Kleint committed
980
981
982
983
984
static inline QString msgCannotLaunch(const QString &binary)
{
    return GitClient::tr("Cannot launch \"%1\".").arg(QDir::toNativeSeparators(binary));
}

985
986
static inline QString currentDocumentPath()
{
987
988
    if (Core::IDocument *document= Core::EditorManager::currentDocument())
        return QFileInfo(document->filePath()).path();
989
    return QString();
990
991
}

Orgad Shaneh's avatar
Orgad Shaneh committed
992
993
994
995
996
997
static inline QStringList statusArguments()
{
    return QStringList() << QLatin1String("-c") << QLatin1String("color.status=false")
                         << QLatin1String("status");
}

998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
static inline void msgCannotRun(const QString &message, QString *errorMessage)
{
    if (errorMessage)
        *errorMessage = message;
    else
        outputWindow()->appendError(message);
}

static inline void msgCannotRun(const QStringList &args, const QString &workingDirectory,
                                const QByteArray &error, QString *errorMessage)
{
    const QString message = GitClient::tr("Cannot run \"%1 %2\" in \"%2\": %3")
            .arg(QLatin1String("git ") + args.join(QLatin1String(" ")),
                 QDir::toNativeSeparators(workingDirectory),
                 commandOutputFromLocal8Bit(error));

    msgCannotRun(message, errorMessage);
}

1017
// ---------------- GitClient
1018
1019
1020

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

1021
1022
GitClient::GitClient(GitSettings *settings) :
    m_cachedGitVersion(0),
Tobias Hunger's avatar
Tobias Hunger committed
1023
    m_msgWait(tr("Waiting for data...")),
1024
1025
    m_settings(settings),
    m_disableEditor(false)
con's avatar
con committed
1026
{
1027
    QTC_CHECK(settings);
hjk's avatar
hjk committed
1028
    connect(Core::ICore::instance(), SIGNAL(saveSettingsRequested()), this, SLOT(saveSettings()));
1029
1030
1031
    m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
            .arg(QCoreApplication::applicationFilePath())
            .arg(QCoreApplication::applicationPid());
con's avatar
con committed
1032
1033
1034
1035
1036
1037
1038
1039
}

GitClient::~GitClient()
{
}

QString GitClient::findRepositoryForDirectory(const QString &dir)
{
1040
1041
    if (dir.isEmpty() || dir.endsWith(QLatin1String("/.git"))
            || dir.contains(QLatin1String("/.git/"))) {
1042
        return QString();
1043
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
1044
    QDir directory(dir);
1045
1046
1047
    QString dotGit = QLatin1String(GIT_DIRECTORY);
    // QFileInfo is outside loop, because it is faster this way
    QFileInfo fileInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
1048
    do {
1049
1050
1051
1052
1053
1054
1055
        if (directory.exists(dotGit)) {
            fileInfo.setFile(directory, dotGit);
            if (fileInfo.isFile())
                return directory.absolutePath();
            else if