gitclient.cpp 109 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>

42
#include <coreplugin/actionmanager/actionmanager.h>
hjk's avatar
hjk committed
43
#include <coreplugin/coreconstants.h>
con's avatar
con committed
44
#include <coreplugin/editormanager/editormanager.h>
hjk's avatar
hjk committed
45
46
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
47
#include <coreplugin/progressmanager/progressmanager.h>
48
#include <coreplugin/vcsmanager.h>
49
#include <coreplugin/id.h>
50
#include <coreplugin/documentmanager.h>
51
52
#include <coreplugin/iversioncontrol.h>

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

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

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

80
static const char GIT_DIRECTORY[] = ".git";
con's avatar
con committed
81

82
83
84
namespace Git {
namespace Internal {

hjk's avatar
hjk committed
85
class BaseGitDiffArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
86
{
Friedemann Kleint's avatar
Friedemann Kleint committed
87
    Q_OBJECT
88

89
public:
90
    BaseGitDiffArgumentsWidget(GitClient *client, const QString &directory,
91
                               const QStringList &args) :
92
        m_workingDirectory(directory),
93
        m_client(client)
94
    {
95
96
        QTC_ASSERT(!directory.isEmpty(), return);
        QTC_ASSERT(m_client, return);
97

98
99
100
        m_patienceButton = addToggleButton(QLatin1String("--patience"), tr("Patience"),
                                           tr("Use the patience algorithm for calculating the differences."));
        mapSetting(m_patienceButton, client->settings()->boolPointer(GitSettings::diffPatienceKey));
101
        m_ignoreWSButton = addToggleButton(QLatin1String("--ignore-space-change"), tr("Ignore Whitespace"),
102
103
                                           tr("Ignore whitespace only changes."));
        mapSetting(m_ignoreWSButton, m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInDiffKey));
104
105

        setBaseArguments(args);
106
107
    }

108
protected:
109
110
    QString m_workingDirectory;
    GitClient *m_client;
111
112
    QToolButton *m_patienceButton;
    QToolButton *m_ignoreWSButton;
113
114
115
116
};

class GitCommitDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
117
    Q_OBJECT
118

119
public:
120
    GitCommitDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
121
122
                                 const QStringList &args, const QStringList &unstaged,
                                 const QStringList &staged) :
123
        BaseGitDiffArgumentsWidget(client, directory, args),
124
125
126
127
        m_unstagedFileNames(unstaged),
        m_stagedFileNames(staged)
    { }

128
    void executeCommand()
129
    {
130
        m_client->diff(m_workingDirectory, arguments(), m_unstagedFileNames, m_stagedFileNames);
131
132
133
134
135
136
137
138
139
    }

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

class GitFileDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
140
    Q_OBJECT
141
public:
142
    GitFileDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
143
                               const QStringList &args, const QString &file) :
144
        BaseGitDiffArgumentsWidget(client, directory, args),
145
146
147
        m_fileName(file)
    { }

148
    void executeCommand()
149
    {
150
        m_client->diff(m_workingDirectory, arguments(), m_fileName);
151
152
153
154
155
156
157
158
    }

private:
    const QString m_fileName;
};

class GitBranchDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
159
    Q_OBJECT
160
public:
161
    GitBranchDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
162
                                 const QStringList &args, const QString &branch) :
163
        BaseGitDiffArgumentsWidget(client, directory, args),
164
165
166
        m_branchName(branch)
    { }

167
    void executeCommand()
168
    {
169
        m_client->diffBranch(m_workingDirectory, arguments(), m_branchName);
170
171
172
173
174
175
    }

private:
    const QString m_branchName;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
176
class GitShowArgumentsWidget : public BaseGitDiffArgumentsWidget
177
{
Friedemann Kleint's avatar
Friedemann Kleint committed
178
    Q_OBJECT
179

180
public:
181
    GitShowArgumentsWidget(Git::Internal::GitClient *client,
182
183
184
                           const QString &directory,
                           const QStringList &args,
                           const QString &id) :
Orgad Shaneh's avatar
Orgad Shaneh committed
185
        BaseGitDiffArgumentsWidget(client, directory, args),
186
187
        m_client(client),
        m_workingDirectory(directory),
188
189
        m_id(id)
    {
190
191
192
193
194
195
196
197
198
        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"));
        mapSetting(addComboBox(QLatin1String("--pretty"), prettyChoices),
199
                   m_client->settings()->intPointer(GitSettings::showPrettyFormatKey));
200
201
202
    }

    void executeCommand()
203
    {
204
        m_client->show(m_workingDirectory, m_id, arguments());
205
206
207
    }

private:
208
209
    GitClient *m_client;
    QString m_workingDirectory;
210
    QString m_id;
211
212
};

hjk's avatar
hjk committed
213
class GitBlameArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
214
{
Friedemann Kleint's avatar
Friedemann Kleint committed
215
    Q_OBJECT
216

217
public:
218
219
220
221
    GitBlameArgumentsWidget(Git::Internal::GitClient *client,
                            const QString &directory,
                            const QStringList &args,
                            const QString &revision, const QString &fileName) :
222
        m_editor(0),
223
224
        m_client(client),
        m_workingDirectory(directory),
225
226
227
        m_revision(revision),
        m_fileName(fileName)
    {
228
229
230
        mapSetting(addToggleButton(QString(), tr("Omit Date"),
                                   tr("Hide the date of a change from the output.")),
                   m_client->settings()->boolPointer(GitSettings::omitAnnotationDateKey));
231
        mapSetting(addToggleButton(QLatin1String("-w"), tr("Ignore Whitespace"),
232
233
                                   tr("Ignore whitespace only changes.")),
                   m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
234
235

        setBaseArguments(args);
236
237
    }

hjk's avatar
hjk committed
238
    void setEditor(VcsBase::VcsBaseEditorWidget *editor)
239
    {
240
        QTC_ASSERT(editor, return);
241
242
243
        m_editor = editor;
    }

244
    void executeCommand()
245
    {
246
247
248
        int line = -1;
        if (m_editor)
            line = m_editor->lineNumberOfCurrentEditor();
249
        m_client->blame(m_workingDirectory, arguments(), m_fileName, m_revision, line);
250
251
252
    }

private:
hjk's avatar
hjk committed
253
    VcsBase::VcsBaseEditorWidget *m_editor;
254
255
    GitClient *m_client;
    QString m_workingDirectory;
256
257
258
259
    QString m_revision;
    QString m_fileName;
};

260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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)
    {
        QToolButton *button = addToggleButton(QLatin1String("--patch"), tr("Show Diff"),
                                              tr("Show difference."));
        mapSetting(button, m_client->settings()->boolPointer(GitSettings::logDiffKey));
        connect(button, SIGNAL(toggled(bool)), m_patienceButton, SLOT(setEnabled(bool)));
        connect(button, SIGNAL(toggled(bool)), m_ignoreWSButton, SLOT(setEnabled(bool)));
        m_patienceButton->setEnabled(button->isChecked());
        m_ignoreWSButton->setEnabled(button->isChecked());
    }

    void executeCommand()
    {
287
        m_client->log(m_workingDirectory, m_fileNames, m_enableAnnotationContextMenu, arguments());
288
289
290
291
292
293
294
295
296
    }

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

Orgad Shaneh's avatar
Orgad Shaneh committed
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
334
335
336
337
338
339
340
341
342
class ConflictHandler : public QObject
{
    Q_OBJECT
public:
    ConflictHandler(QObject *parent,
                    const QString &workingDirectory,
                    const QString &command)
        : QObject(parent),
          m_workingDirectory(workingDirectory),
          m_command(command)
    {
    }

    ~ConflictHandler()
    {
        if (m_commit.isEmpty())
            GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(m_workingDirectory);
        else
            GitPlugin::instance()->gitClient()->handleMergeConflicts(
                        m_workingDirectory, m_commit, m_command);
    }

    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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
class RebaseManager : public QObject
{
    Q_OBJECT

public:
    RebaseManager(QObject *parent) : QObject(parent)
    {
    }

public slots:
    void readStdErr(const QString &error)
    {
        // rebase conflict is output to stdOut
        QRegExp conflictedCommit(QLatin1String("Could not apply ([^\\n]*)"));
        conflictedCommit.indexIn(error);
        m_commit = conflictedCommit.cap(1);
    }

    void finished(bool ok, int exitCode, const QVariant &workingDirectory)
    {
        Q_UNUSED(ok);
        if (exitCode != 0 && !m_commit.isEmpty()) {
            GitPlugin::instance()->gitClient()->handleMergeConflicts(
                        workingDirectory.toString(), m_commit, QLatin1String("rebase"));
        }
    }

private:
    QString m_commit;
};

hjk's avatar
hjk committed
374
Core::IEditor *locateEditor(const char *property, const QString &entry)
con's avatar
con committed
375
{
hjk's avatar
hjk committed
376
    foreach (Core::IEditor *ed, Core::ICore::editorManager()->openedEditors())
377
        if (ed->document()->property(property).toString() == entry)
con's avatar
con committed
378
379
380
381
            return ed;
    return 0;
}

382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// 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
402
static inline VcsBase::VcsBaseOutputWindow *outputWindow()
403
{
hjk's avatar
hjk committed
404
    return VcsBase::VcsBaseOutputWindow::instance();
405
406
}

407
408
static inline QString msgRepositoryNotFound(const QString &dir)
{
Tobias Hunger's avatar
Tobias Hunger committed
409
    return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
410
411
412
413
}

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

417
418
static inline QString currentDocumentPath()
{
419
420
421
    if (Core::IEditor *editor = Core::EditorManager::currentEditor())
        return QFileInfo(editor->document()->fileName()).path();
    return QString();
422
423
}

424
// ---------------- GitClient
425
426
427

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

428
429
GitClient::GitClient(GitSettings *settings) :
    m_cachedGitVersion(0),
Tobias Hunger's avatar
Tobias Hunger committed
430
    m_msgWait(tr("Waiting for data...")),
431
    m_repositoryChangedSignalMapper(0),
432
    m_settings(settings)
con's avatar
con committed
433
{
434
    QTC_CHECK(settings);
hjk's avatar
hjk committed
435
    connect(Core::ICore::instance(), SIGNAL(saveSettingsRequested()), this, SLOT(saveSettings()));
436
437
438
    m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
            .arg(QCoreApplication::applicationFilePath())
            .arg(QCoreApplication::applicationPid());
con's avatar
con committed
439
440
441
442
443
444
}

GitClient::~GitClient()
{
}

445
const char *GitClient::noColorOption = "--no-color";
446
const char *GitClient::decorateOption = "--decorate";
447

con's avatar
con committed
448
449
QString GitClient::findRepositoryForDirectory(const QString &dir)
{
450
451
    if (dir.endsWith(QLatin1String("/.git")) || dir.contains(QLatin1String("/.git/")))
        return QString();
Orgad Shaneh's avatar
Orgad Shaneh committed
452
    QDir directory(dir);
453
454
455
    QString dotGit = QLatin1String(GIT_DIRECTORY);
    // QFileInfo is outside loop, because it is faster this way
    QFileInfo fileInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
456
    do {
457
458
459
460
461
462
463
        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
464
    } while (directory.cdUp());
465
    return QString();
con's avatar
con committed
466
467
}

Orgad Shaneh's avatar
Orgad Shaneh committed
468
QString GitClient::findGitDirForRepository(const QString &repositoryDir) const
Orgad Shaneh's avatar
Orgad Shaneh committed
469
{
470
471
472
473
    static QHash<QString, QString> repoDirCache;
    QString &res = repoDirCache[repositoryDir];
    if (!res.isEmpty())
        return res;
Orgad Shaneh's avatar
Orgad Shaneh committed
474
475
476
477
    QByteArray outputText;
    QStringList arguments;
    arguments << QLatin1String("rev-parse") << QLatin1String("--git-dir");
    fullySynchronousGit(repositoryDir, arguments, &outputText, 0, false);
478
    res = QString::fromLocal8Bit(outputText.trimmed());
479
480
481
    if (!QDir(res).isAbsolute())
        res.prepend(repositoryDir + QLatin1Char('/'));
    return res;
Orgad Shaneh's avatar
Orgad Shaneh committed
482
483
}

hjk's avatar
hjk committed
484
VcsBase::VcsBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
Tobias Hunger's avatar
Tobias Hunger committed
485
                                                               const QString &dynamicPropertyValue) const
486
{
hjk's avatar
hjk committed
487
    VcsBase::VcsBaseEditorWidget *rc = 0;
hjk's avatar
hjk committed
488
    Core::IEditor *outputEditor = locateEditor(registerDynamicProperty, dynamicPropertyValue);
489
490
491
    if (!outputEditor)
        return 0;

492
    // Exists already
hjk's avatar
hjk committed
493
    Core::EditorManager::activateEditor(outputEditor, Core::EditorManager::ModeSwitch);
494
    outputEditor->createNew(m_msgWait);
hjk's avatar
hjk committed
495
    rc = VcsBase::VcsBaseEditorWidget::getVcsBaseEditor(outputEditor);
496
497
498
499

    return rc;
}

con's avatar
con committed
500
501
502
503
/* 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
504
VcsBase::VcsBaseEditorWidget *GitClient::createVcsEditor(const Core::Id &id,
Tobias Hunger's avatar
Tobias Hunger committed
505
506
507
                                                         QString title,
                                                         // Source file or directory
                                                         const QString &source,
508
                                                         CodecType codecType,
Tobias Hunger's avatar
Tobias Hunger committed
509
510
511
512
                                                         // Dynamic property and value to identify that editor
                                                         const char *registerDynamicProperty,
                                                         const QString &dynamicPropertyValue,
                                                         QWidget *configWidget) const
con's avatar
con committed
513
{
hjk's avatar
hjk committed
514
    VcsBase::VcsBaseEditorWidget *rc = 0;
515
    QTC_CHECK(!findExistingVCSEditor(registerDynamicProperty, dynamicPropertyValue));
516
517

    // Create new, set wait message, set up with source and codec
hjk's avatar
hjk committed
518
    Core::IEditor *outputEditor = Core::EditorManager::openEditorWithContents(id, &title, m_msgWait);
519
    outputEditor->document()->setProperty(registerDynamicProperty, dynamicPropertyValue);
hjk's avatar
hjk committed
520
    rc = VcsBase::VcsBaseEditorWidget::getVcsBaseEditor(outputEditor);
521
522
523
524
    connect(rc, SIGNAL(annotateRevisionRequested(QString,QString,int)),
            this, SLOT(slotBlameRevisionRequested(QString,QString,int)));
    QTC_ASSERT(rc, return 0);
    rc->setSource(source);
525
    if (codecType == CodecSource) {
526
        rc->setCodec(getSourceCodec(source));
527
528
529
530
531
532
    } else if (codecType == CodecLogOutput) {
        QString encodingName = readConfigValue(source, QLatin1String("i18n.logOutputEncoding"));
        if (encodingName.isEmpty())
            encodingName = QLatin1String("utf-8");
        rc->setCodec(QTextCodec::codecForName(encodingName.toLocal8Bit()));
    }
533

534
    rc->setForceReadOnly(true);
hjk's avatar
hjk committed
535
    Core::EditorManager::activateEditor(outputEditor, Core::EditorManager::ModeSwitch);
536
537
538
539

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

con's avatar
con committed
540
541
542
    return rc;
}

543
544
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
545
546
                     const QStringList &unstagedFileNames,
                     const QStringList &stagedFileNames)
con's avatar
con committed
547
{
548
    const QString binary = settings()->stringValue(GitSettings::binaryPathKey);
hjk's avatar
hjk committed
549
    const Core::Id editorId = Git::Constants::GIT_DIFF_EDITOR_ID;
con's avatar
con committed
550
551
    const QString title = tr("Git Diff");

hjk's avatar
hjk committed
552
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", workingDirectory);
553
554
    if (!editor) {
        GitCommitDiffArgumentsWidget *argWidget =
555
                new GitCommitDiffArgumentsWidget(this, workingDirectory, diffArgs,
556
557
                                                 unstagedFileNames, stagedFileNames);

hjk's avatar
hjk committed
558
        editor = createVcsEditor(editorId, title,
559
                                 workingDirectory, CodecSource, "originalFileName", workingDirectory, argWidget);
hjk's avatar
hjk committed
560
        connect(editor, SIGNAL(diffChunkReverted(VcsBase::DiffChunk)), argWidget, SLOT(executeCommand()));
561
    }
562
563
564

    GitCommitDiffArgumentsWidget *argWidget = qobject_cast<GitCommitDiffArgumentsWidget *>(editor->configurationWidget());
    QStringList userDiffArgs = argWidget->arguments();
565
    editor->setDiffBaseDirectory(workingDirectory);
con's avatar
con committed
566

567
568
569
    // 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.
hjk's avatar
hjk committed
570
    VcsBase::Command *command = createCommand(workingDirectory, editor);
571
    // Directory diff?
572
573
574
575

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

576
577
    int timeout = settings()->intValue(GitSettings::timeoutKey);

578
    if (unstagedFileNames.empty() && stagedFileNames.empty()) {
579
580
       QStringList arguments(cmdArgs);
       arguments << userDiffArgs;
581
       outputWindow()->appendCommand(workingDirectory, binary, arguments);
582
       command->addJob(arguments, timeout);
583
584
585
    } else {
        // Files diff.
        if (!unstagedFileNames.empty()) {
586
587
           QStringList arguments(cmdArgs);
           arguments << userDiffArgs;
588
           arguments << QLatin1String("--") << unstagedFileNames;
589
           outputWindow()->appendCommand(workingDirectory, binary, arguments);
590
           command->addJob(arguments, timeout);
591
592
        }
        if (!stagedFileNames.empty()) {
593
594
           QStringList arguments(cmdArgs);
           arguments << userDiffArgs;
595
           arguments << QLatin1String("--cached") << diffArgs << QLatin1String("--") << stagedFileNames;
596
           outputWindow()->appendCommand(workingDirectory, binary, arguments);
597
           command->addJob(arguments, timeout);
598
599
600
        }
    }
    command->execute();
con's avatar
con committed
601
602
}

603
604
605
void GitClient::diff(const QString &workingDirectory,
                     const QStringList &diffArgs,
                     const QString &fileName)
con's avatar
con committed
606
{
hjk's avatar
hjk committed
607
    const Core::Id editorId = Git::Constants::GIT_DIFF_EDITOR_ID;
Tobias Hunger's avatar
Tobias Hunger committed
608
    const QString title = tr("Git Diff \"%1\"").arg(fileName);
hjk's avatar
hjk committed
609
    const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, fileName);
610

hjk's avatar
hjk committed
611
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", sourceFile);
612
613
    if (!editor) {
        GitFileDiffArgumentsWidget *argWidget =
614
                new GitFileDiffArgumentsWidget(this, workingDirectory, diffArgs, fileName);
615

hjk's avatar
hjk committed
616
617
        editor = createVcsEditor(editorId, title, sourceFile, CodecSource, "originalFileName", sourceFile, argWidget);
        connect(editor, SIGNAL(diffChunkReverted(VcsBase::DiffChunk)), argWidget, SLOT(executeCommand()));
618
    }
Tobias Hunger's avatar
Tobias Hunger committed
619
    editor->setDiffBaseDirectory(workingDirectory);
620

621
622
623
    GitFileDiffArgumentsWidget *argWidget = qobject_cast<GitFileDiffArgumentsWidget *>(editor->configurationWidget());
    QStringList userDiffArgs = argWidget->arguments();

624
625
626
627
628
629
630
    QStringList cmdArgs;
    cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
              << userDiffArgs;

    if (!fileName.isEmpty())
        cmdArgs << QLatin1String("--") << fileName;
    executeGit(workingDirectory, cmdArgs, editor);
con's avatar
con committed
631
632
}

633
634
635
636
void GitClient::diffBranch(const QString &workingDirectory,
                           const QStringList &diffArgs,
                           const QString &branchName)
{
hjk's avatar
hjk committed
637
    const Core::Id editorId = Git::Constants::GIT_DIFF_EDITOR_ID;
Tobias Hunger's avatar
Tobias Hunger committed
638
    const QString title = tr("Git Diff Branch \"%1\"").arg(branchName);
hjk's avatar
hjk committed
639
    const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, QStringList());
640

hjk's avatar
hjk committed
641
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("BranchName", branchName);
642
    if (!editor)
hjk's avatar
hjk committed
643
        editor = createVcsEditor(editorId, title, sourceFile, CodecSource, "BranchName", branchName,
644
                                 new GitBranchDiffArgumentsWidget(this, workingDirectory,
645
                                                                  diffArgs, branchName));
Tobias Hunger's avatar
Tobias Hunger committed
646
    editor->setDiffBaseDirectory(workingDirectory);
647

648
649
    GitBranchDiffArgumentsWidget *argWidget = qobject_cast<GitBranchDiffArgumentsWidget *>(editor->configurationWidget());
    QStringList userDiffArgs = argWidget->arguments();
650
651
652

    QStringList cmdArgs;
    cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
653
              << userDiffArgs  << branchName;
654
655

    executeGit(workingDirectory, cmdArgs, editor);
656
657
}

658
659
660
661
662
663
664
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
665
666
void GitClient::status(const QString &workingDirectory)
{
667
    // @TODO: Use "--no-color" once it is supported
668
669
    QStringList statusArgs(QLatin1String("status"));
    statusArgs << QLatin1String("-u");
hjk's avatar
hjk committed
670
    VcsBase::VcsBaseOutputWindow *outwin = outputWindow();
671
    outwin->setRepository(workingDirectory);
hjk's avatar
hjk committed
672
    VcsBase::Command *command = executeGit(workingDirectory, statusArgs, 0, true);
673
    connect(command, SIGNAL(finished(bool,int,QVariant)), outwin, SLOT(clearRepository()),
674
            Qt::QueuedConnection);
con's avatar
con committed
675
676
}

677
static const char graphLogFormatC[] = "%h %d %an %s %ci";
678
679

// Create a graphical log.
680
void GitClient::graphLog(const QString &workingDirectory, const QString & branch)
681
682
683
684
{
    QStringList arguments;
    arguments << QLatin1String("log") << QLatin1String(noColorOption);

685
686
687
    int logCount = settings()->intValue(GitSettings::logCountKey);
    if (logCount > 0)
         arguments << QLatin1String("-n") << QString::number(logCount);
688
689
690
    arguments << (QLatin1String("--pretty=format:") +  QLatin1String(graphLogFormatC))
              << QLatin1String("--topo-order") <<  QLatin1String("--graph");

691
692
693
694
    QString title;
    if (branch.isEmpty()) {
        title = tr("Git Log");
    } else {
Tobias Hunger's avatar
Tobias Hunger committed
695
        title = tr("Git Log \"%1\"").arg(branch);
696
697
        arguments << branch;
    }
hjk's avatar
hjk committed
698
    const Core::Id editorId = Git::Constants::GIT_LOG_EDITOR_ID;
hjk's avatar
hjk committed
699
700
    const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, QStringList());
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("logFileName", sourceFile);
701
    if (!editor)
hjk's avatar
hjk committed
702
        editor = createVcsEditor(editorId, title, sourceFile, CodecLogOutput, "logFileName", sourceFile, 0);
703
704
705
    executeGit(workingDirectory, arguments, editor);
}

706
void GitClient::log(const QString &workingDirectory, const QStringList &fileNames,
707
                    bool enableAnnotationContextMenu, const QStringList &args)
con's avatar
con committed
708
{
709
    const QString msgArg = fileNames.empty() ? workingDirectory :
710
                           fileNames.join(QLatin1String(", "));
711
712
    const QString title = tr("Git Log \"%1\"").arg(msgArg);
    const Core::Id editorId = Git::Constants::GIT_LOG_EDITOR_ID;
hjk's avatar
hjk committed
713
714
    const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, fileNames);
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("logFileName", sourceFile);
715
    if (!editor)
hjk's avatar
hjk committed
716
        editor = createVcsEditor(editorId, title, sourceFile, CodecLogOutput, "logFileName", sourceFile,
717
718
719
720
721
                                 new GitLogArgumentsWidget(this, workingDirectory,
                                                           enableAnnotationContextMenu,
                                                           args, fileNames));
    editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);

722
    QStringList arguments;
723
724
    arguments << QLatin1String("log") << QLatin1String(noColorOption)
              << QLatin1String(decorateOption);
725

726
727
728
    int logCount = settings()->intValue(GitSettings::logCountKey);
    if (logCount > 0)
         arguments << QLatin1String("-n") << QString::number(logCount);
729

730
731
732
733
734
    GitLogArgumentsWidget *argWidget = qobject_cast<GitLogArgumentsWidget *>(editor->configurationWidget());
    QStringList userArgs = argWidget->arguments();

    arguments.append(userArgs);

735
    if (!fileNames.isEmpty())
736
        arguments << QLatin1String("--") << fileNames;
con's avatar
con committed
737

738
    executeGit(workingDirectory, arguments, editor);
con's avatar
con committed
739
740
}

741
742
743
744
745
746
747
748
749
750
751
752
// Do not show "0000" or "^32ae4"
static inline bool canShow(const QString &sha)
{
    if (sha.startsWith(QLatin1Char('^')))
        return false;
    if (sha.count(QLatin1Char('0')) == sha.size())
        return false;
    return true;
}

static inline QString msgCannotShow(const QString &sha)
{
Tobias Hunger's avatar
Tobias Hunger committed
753
    return GitClient::tr("Cannot describe \"%1\".").arg(sha);
754
755
}

756
void GitClient::show(const QString &source, const QString &id, const QStringList &args)
con's avatar
con committed
757
{
758
759
760
761
762
    if (!canShow(id)) {
        outputWindow()->append(msgCannotShow(id));
        return;
    }

Tobias Hunger's avatar
Tobias Hunger committed
763
    const QString title = tr("Git Show \"%1\"").arg(id);
hjk's avatar
hjk committed
764
    const Core::Id editorId = Git::Constants::GIT_DIFF_EDITOR_ID;
hjk's avatar
hjk committed
765
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("show", id);
766
    if (!editor)
hjk's avatar
hjk committed
767
        editor = createVcsEditor(editorId, title, source, CodecSource, "show", id,
768
                                 new GitShowArgumentsWidget(this, source, args, id));
769
770
771

    GitShowArgumentsWidget *argWidget = qobject_cast<GitShowArgumentsWidget *>(editor->configurationWidget());
    QStringList userArgs = argWidget->arguments();
772
773
774

    QStringList arguments;
    arguments << QLatin1String("show") << QLatin1String(noColorOption);
775
    arguments << QLatin1String(decorateOption);
776
777
    arguments.append(userArgs);
    arguments << id;
con's avatar
con committed
778
779
780

    const QFileInfo sourceFi(source);
    const QString workDir = sourceFi.isDir() ? sourceFi.absoluteFilePath() : sourceFi.absolutePath();
Tobias Hunger's avatar
Tobias Hunger committed
781
    editor->setDiffBaseDirectory(workDir);
782
    executeGit(workDir, arguments, editor);
con's avatar
con committed
783
784
}

Tobias Hunger's avatar
Tobias Hunger committed
785
786
void GitClient::saveSettings()
{
hjk's avatar
hjk committed
787
    settings()->writeSettings(Core::ICore::settings());
Tobias Hunger's avatar
Tobias Hunger committed
788
789
}

790
791
792
793
794
795
796
797
void GitClient::slotBlameRevisionRequested(const QString &source, QString change, int lineNumber)
{
    // This might be invoked with a verbose revision description
    // "SHA1 author subject" from the annotation context menu. Strip the rest.
    const int blankPos = change.indexOf(QLatin1Char(' '));
    if (blankPos != -1)
        change.truncate(blankPos);
    const QFileInfo fi(source);
798
    blame(fi.absolutePath(), QStringList(), fi.fileName(), change, lineNumber);
799
800
}

801
802
803
804
805
806
807
808
809
810
811
812
void GitClient::appendOutputData(const QByteArray &data) const
{
    const QTextCodec *codec = getSourceCodec(currentDocumentPath());
    outputWindow()->appendData(codec->toUnicode(data).toLocal8Bit());
}

void GitClient::appendOutputDataSilently(const QByteArray &data) const
{
    const QTextCodec *codec = getSourceCodec(currentDocumentPath());
    outputWindow()->appendDataSilently(codec->toUnicode(data).toLocal8Bit());
}

813
814
815
816
817
818
819
820
821
822
QTextCodec *GitClient::getSourceCodec(const QString &file) const
{
    if (QFileInfo(file).isFile())
        return VcsBase::VcsBaseEditorWidget::getCodec(file);
    QString encodingName = readConfigValue(file, QLatin1String("gui.encoding"));
    if (encodingName.isEmpty())
        encodingName = QLatin1String("utf-8");
    return QTextCodec::codecForName(encodingName.toLocal8Bit());
}

823
void GitClient::blame(const QString &workingDirectory,
824
                      const QStringList &args,
825
                      const QString &fileName,
Tobias Hunger's avatar
Tobias Hunger committed
826
827
                      const QString &revision,
                      int lineNumber)
con's avatar
con committed
828
{
hjk's avatar
hjk committed
829
    const Core::Id editorId = Git::Constants::GIT_BLAME_EDITOR_ID;
hjk's avatar
hjk committed
830
    const QString id = VcsBase::VcsBaseEditorWidget::getTitleId(workingDirectory, QStringList(fileName), revision);
Tobias Hunger's avatar
Tobias Hunger committed
831
    const QString title = tr("Git Blame \"%1\"").arg(id);
hjk's avatar
hjk committed
832
    const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, fileName);
con's avatar
con committed
833

hjk's avatar
hjk committed
834
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("blameFileName", id);
835
836
    if (!editor) {
        GitBlameArgumentsWidget *argWidget =
837
                new GitBlameArgumentsWidget(this, workingDirectory, args,
838
                                            revision, fileName);
hjk's avatar
hjk committed
839
        editor = createVcsEditor(editorId, title, sourceFile, CodecSource, "blameFileName", id, argWidget);
840
841
842
        argWidget->setEditor(editor);
    }

843
844
845
    GitBlameArgumentsWidget *argWidget = qobject_cast<GitBlameArgumentsWidget *>(editor->configurationWidget());
    QStringList userBlameArgs = argWidget->arguments();

846
847
848
849
850
851
    QStringList arguments(QLatin1String("blame"));
    arguments << QLatin1String("--root");
    arguments.append(userBlameArgs);
    arguments << QLatin1String("--") << fileName;
    if (!revision.isEmpty())
        arguments << revision;
hjk's avatar
hjk committed
852
    executeGit(workingDirectory, arguments, editor, false, VcsBase::Command::NoReport, lineNumber);
con's avatar
con committed
853
854
}

855
856
bool GitClient::synchronousCheckout(const QString &workingDirectory,
                                          const QString &ref,
Tobias Hunger's avatar
Tobias Hunger committed
857
                                          QString *errorMessage /* = 0 */)
858
859
860
861
{
    QByteArray outputText;
    QByteArray errorText;
    QStringList arguments;
862
    arguments << QLatin1String("checkout") << ref;
863
    const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
864
    const QString output = commandOutputFromLocal8Bit(outputText);
865
    outputWindow()->append(output);
866
867
    if (!rc) {
        const QString stdErr = commandOutputFromLocal8Bit(errorText);
Friedemann Kleint's avatar
Friedemann Kleint committed
868
        //: Meaning of the arguments: %1: Branch, %2: Repository, %3: Error message
869
        const QString msg = tr("Cannot checkout \"%1\" of \"%2\": %3").arg(ref, workingDirectory, stdErr);
870
        if (errorMessage)
871
            *errorMessage = msg;
872
        else
873
            outputWindow()->appendError(msg);
874
875
876
877
878
        return false;
    }
    return true;
}

con's avatar
con committed
879
880
881
882
883
884
885
void GitClient::hardReset(const QString &workingDirectory, const QString &commit)
{
    QStringList arguments;
    arguments << QLatin1String("reset") << QLatin1String("--hard");
    if (!commit.isEmpty())
        arguments << commit;

Petar Perisin's avatar
Petar Perisin committed
886
887
888
889
890
891
892
893
894
895
896
897
    VcsBase::Command *cmd = executeGit(workingDirectory, arguments, 0, true);
    connectRepositoryChanged(workingDirectory, cmd);
}

void GitClient::softReset(const QString &workingDirectory, const QString &commit)
{
    if (commit.isEmpty())
        return;

    QStringList arguments;
    arguments << QLatin1String("reset") << QLatin1String("--soft") << commit;

hjk's avatar
hjk committed
898
    VcsBase::Command *cmd = executeGit(workingDirectory, arguments, 0, true);
899
    connectRepositoryChanged(workingDirectory, cmd);
con's avatar
con committed
900
901
902
903
904
905
906
}

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

907
    executeGit(workingDirectory, arguments, 0, true);
con's avatar
con committed
908
909
}

910
911
912
913
914
915
916
917
918
919
bool GitClient::synchronousLog(const QString &workingDirectory, const QStringList &arguments,
                               QString *output, QString *errorMessageIn)
{
    QByteArray outputText;
    QByteArray errorText;
    QStringList allArguments;
    allArguments << QLatin1String("log") << QLatin1String(GitClient::noColorOption);
    allArguments.append(arguments);
    const bool rc = fullySynchronousGit(workingDirectory, allArguments, &outputText, &errorText);
    if (rc) {
Yuchen Deng's avatar
Yuchen Deng committed
920
921
922
923
924
925
926
927
        QString encodingName = readConfigValue(workingDirectory, QLatin1String("i18n.logOutputEncoding"));
        if (encodingName.isEmpty())
            encodingName = QLatin1String("utf-8");
        QTextCodec *codec = QTextCodec::codecForName(encodingName.toLocal8Bit());
        if (codec)
            *output = codec->toUnicode(outputText);
        else
            *output = commandOutputFromLocal8Bit(outputText);
928
929
930
931
    } else {
        const QString errorMessage = tr("Cannot obtain log of \"%1\": %2").
                                     arg(QDir::toNativeSeparators(workingDirectory),
                                         commandOutputFromLocal8Bit(errorText));
932
        if (errorMessageIn)
933
            *errorMessageIn = errorMessage;
934
        else
935
936
937
938
939