gitclient.cpp 124 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
Eike Ziller's avatar
Eike Ziller committed
3
4
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing
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
** 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
Eike Ziller's avatar
Eike Ziller committed
12
13
** a written agreement between you and The Qt Company.  For licensing terms and
** conditions see http://www.qt.io/terms-conditions.  For further information
Eike Ziller's avatar
Eike Ziller committed
14
** use the contact form at http://www.qt.io/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
** Alternatively, this file may be used under the terms of the GNU Lesser
Eike Ziller's avatar
Eike Ziller committed
18
19
20
21
22
23
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file.  Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
hjk's avatar
hjk committed
24
**
Eike Ziller's avatar
Eike Ziller committed
25
26
** In addition, as a special exception, The Qt Company gives you certain additional
** rights.  These rights are described in The Qt Company LGPL Exception
con's avatar
con committed
27
28
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
29
****************************************************************************/
hjk's avatar
hjk committed
30

con's avatar
con committed
31
#include "gitclient.h"
32
#include "gitutils.h"
hjk's avatar
hjk committed
33

con's avatar
con committed
34
#include "commitdata.h"
hjk's avatar
hjk committed
35
#include "gitconstants.h"
36
#include "giteditor.h"
hjk's avatar
hjk committed
37
#include "gitplugin.h"
38
#include "gitsubmiteditor.h"
39
#include "gitversioncontrol.h"
40
#include "mergetool.h"
41
#include "branchadddialog.h"
Christian Kandeler's avatar
Christian Kandeler committed
42
#include "gerrit/gerritplugin.h"
con's avatar
con committed
43

44
45
#include <vcsbase/submitfilemodel.h>

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

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/fileutils.h>
hjk's avatar
hjk committed
59
#include <vcsbase/vcscommand.h>
hjk's avatar
hjk committed
60
#include <vcsbase/vcsbaseeditor.h>
61
#include <vcsbase/vcsbaseeditorparameterwidget.h>
62
#include <vcsbase/vcsoutputwindow.h>
63
#include <vcsbase/vcsbaseplugin.h>
con's avatar
con committed
64

65
#include <diffeditor/diffeditorconstants.h>
jkobus's avatar
jkobus committed
66
#include <diffeditor/diffeditorcontroller.h>
jkobus's avatar
jkobus committed
67
#include <diffeditor/diffutils.h>
68

69
#include <QCoreApplication>
70
#include <QDir>
71
#include <QFileInfo>
72
#include <QHash>
73
#include <QRegExp>
74
#include <QSignalMapper>
jkobus's avatar
jkobus committed
75
#include <QTemporaryFile>
con's avatar
con committed
76

77
78
#include <QAction>
#include <QMenu>
79
#include <QMessageBox>
80
#include <QPushButton>
81
82
#include <QToolButton>
#include <QTextCodec>
con's avatar
con committed
83

84
static const char GIT_DIRECTORY[] = ".git";
85
static const char graphLogFormatC[] = "%h %d %an %s %ci";
86
static const char HEAD[] = "HEAD";
87
static const char CHERRY_PICK_HEAD[] = "CHERRY_PICK_HEAD";
88
89
static const char noColorOption[] = "--no-color";
static const char decorateOption[] = "--decorate";
con's avatar
con committed
90

Orgad Shaneh's avatar
Orgad Shaneh committed
91
using namespace Core;
Tobias Hunger's avatar
Tobias Hunger committed
92
using namespace DiffEditor;
hjk's avatar
hjk committed
93
using namespace Utils;
94
using namespace VcsBase;
Orgad Shaneh's avatar
Orgad Shaneh committed
95

96
97
98
namespace Git {
namespace Internal {

99
// Suppress git diff warnings about "LF will be replaced by CRLF..." on Windows.
hjk's avatar
hjk committed
100
static unsigned diffExecutionFlags()
101
{
hjk's avatar
hjk committed
102
    return HostOsInfo::isWindowsHost() ? unsigned(VcsBasePlugin::SuppressStdErrInLogWindow) : 0u;
103
104
}

105
106
107
/////////////////////////////////////

class BaseController : public DiffEditorController
108
109
110
111
{
    Q_OBJECT

public:
112
113
114
    BaseController(IDocument *document, const QString &dir);
    ~BaseController();
    void runCommand(const QList<QStringList> &args, QTextCodec *codec = 0);
115
116

private slots:
117
    virtual void processOutput(const QString &output);
118

119
120
121
122
protected:
    void processDiff(const QString &output);
    QStringList addConfigurationArguments(const QStringList &args) const;
    GitClient *gitClient() const;
123
    QStringList addHeadWhenCommandInProgress() const;
124

125
    const QString m_directory;
126

127
128
private:
    VcsCommand *m_command;
129
130
};

131
132
133
134
135
BaseController::BaseController(IDocument *document, const QString &dir) :
    DiffEditorController(document),
    m_directory(dir),
    m_command(0)
{ }
136

137
BaseController::~BaseController()
138
{
139
140
    if (m_command)
        m_command->cancel();
141
142
}

143
void BaseController::runCommand(const QList<QStringList> &args, QTextCodec *codec)
144
{
145
146
147
    if (m_command) {
        m_command->disconnect();
        m_command->cancel();
148
    }
149

150
    m_command = new VcsCommand(gitClient()->vcsBinary(), m_directory, gitClient()->processEnvironment());
151
152
153
154
    m_command->setCodec(codec ? codec : EditorManager::defaultTextCodec());
    connect(m_command, &VcsCommand::output, this, &BaseController::processOutput);
    connect(m_command, &VcsCommand::finished, this, &BaseController::reloadFinished);
    m_command->addFlags(diffExecutionFlags());
155

156
157
    foreach (const QStringList &arg, args) {
        QTC_ASSERT(!arg.isEmpty(), continue);
158

159
        m_command->addJob(arg, gitClient()->vcsTimeoutS());
160
    }
161

162
    m_command->execute();
163
164
}

165
void BaseController::processDiff(const QString &output)
166
{
167
    m_command = 0;
168

169
170
171
    bool ok;
    QList<FileData> fileDataList = DiffUtils::readPatch(output, &ok);
    setDiffFiles(fileDataList, m_directory);
172
173
}

174
QStringList BaseController::addConfigurationArguments(const QStringList &args) const
175
{
176
    QTC_ASSERT(!args.isEmpty(), return args);
jkobus's avatar
jkobus committed
177

178
179
180
181
182
183
184
185
186
187
188
    QStringList realArgs;
    realArgs << args.at(0);
    realArgs << QLatin1String("-m"); // show diff agains parents instead of merge commits
    realArgs << QLatin1String("--first-parent"); // show only first parent
    if (ignoreWhitespace())
        realArgs << QLatin1String("--ignore-space-change");
    realArgs << QLatin1String("--unified=") + QString::number(contextLineCount());
    realArgs << QLatin1String("--src-prefix=a/") << QLatin1String("--dst-prefix=b/");
    realArgs << args.mid(1);

    return realArgs;
189
190
}

191
void BaseController::processOutput(const QString &output)
192
{
193
    processDiff(output);
194
195
}

196
GitClient *BaseController::gitClient() const
jkobus's avatar
jkobus committed
197
{
198
    return GitPlugin::instance()->client();
jkobus's avatar
jkobus committed
199
200
}

201
QStringList BaseController::addHeadWhenCommandInProgress() const
jkobus's avatar
jkobus committed
202
{
203
204
205
206
    QStringList args;
    // This is workaround for lack of support for merge commits and resolving conflicts,
    // we compare the current state of working tree to the HEAD of current branch
    // instead of showing unsupported combined diff format.
207
    GitClient::CommandInProgress commandInProgress = gitClient()->checkCommandInProgress(m_directory);
208
209
210
    if (commandInProgress != GitClient::NoCommand)
        args << QLatin1String(HEAD);
    return args;
jkobus's avatar
jkobus committed
211
212
}

213
class RepositoryDiffController : public BaseController
214
{
215
216
217
218
219
    Q_OBJECT
public:
    RepositoryDiffController(IDocument *document, const QString &dir) :
        BaseController(document, dir)
    { }
220

Orgad Shaneh's avatar
Orgad Shaneh committed
221
    void reload() override;
222
};
223

224
void RepositoryDiffController::reload()
jkobus's avatar
jkobus committed
225
{
226
227
228
229
    QStringList args;
    args << QLatin1String("diff");
    args.append(addHeadWhenCommandInProgress());
    runCommand(QList<QStringList>() << addConfigurationArguments(args));
jkobus's avatar
jkobus committed
230
231
}

232
class FileDiffController : public BaseController
233
{
234
235
236
237
238
239
    Q_OBJECT
public:
    FileDiffController(IDocument *document, const QString &dir, const QString &fileName) :
        BaseController(document, dir),
        m_fileName(fileName)
    { }
jkobus's avatar
jkobus committed
240

Orgad Shaneh's avatar
Orgad Shaneh committed
241
    void reload() override;
jkobus's avatar
jkobus committed
242

243
244
245
private:
    const QString m_fileName;
};
jkobus's avatar
jkobus committed
246

247
248
249
250
251
252
253
void FileDiffController::reload()
{
    QStringList args;
    args << QLatin1String("diff");
    args.append(addHeadWhenCommandInProgress());
    args << QLatin1String("--") << m_fileName;
    runCommand(QList<QStringList>() << addConfigurationArguments(args));
254
255
}

256
class FileListDiffController : public BaseController
257
{
258
259
260
261
262
263
264
265
    Q_OBJECT
public:
    FileListDiffController(IDocument *document, const QString &dir,
                           const QStringList &stagedFiles, const QStringList &unstagedFiles) :
        BaseController(document, dir),
        m_stagedFiles(stagedFiles),
        m_unstagedFiles(unstagedFiles)
    { }
266

Orgad Shaneh's avatar
Orgad Shaneh committed
267
    void reload() override;
268

269
270
271
272
273
274
private:
    const QStringList m_stagedFiles;
    const QStringList m_unstagedFiles;
};

void FileListDiffController::reload()
275
{
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
    QList<QStringList> argLists;
    if (!m_stagedFiles.isEmpty()) {
        QStringList stagedArgs;
        stagedArgs << QLatin1String("diff") << QLatin1String("--cached") << QLatin1String("--")
                   << m_stagedFiles;
        argLists << addConfigurationArguments(stagedArgs);
    }

    if (!m_unstagedFiles.isEmpty()) {
        QStringList unstagedArgs;
        unstagedArgs << QLatin1String("diff") << addHeadWhenCommandInProgress()
                     << QLatin1String("--") << m_unstagedFiles;
        argLists << addConfigurationArguments(unstagedArgs);
    }

    if (!argLists.isEmpty())
        runCommand(argLists);
293
}
294

295
class ProjectDiffController : public BaseController
296
{
297
298
299
300
301
302
303
304
    Q_OBJECT
public:
    ProjectDiffController(IDocument *document, const QString &dir,
                          const QStringList &projectPaths) :
        BaseController(document, dir),
        m_projectPaths(projectPaths)
    { }

Orgad Shaneh's avatar
Orgad Shaneh committed
305
    void reload() override;
306

307
308
309
310
311
private:
    const QStringList m_projectPaths;
};

void ProjectDiffController::reload()
jkobus's avatar
jkobus committed
312
{
313
314
315
316
    QStringList args;
    args << QLatin1String("diff") << addHeadWhenCommandInProgress()
         << QLatin1String("--") << m_projectPaths;
    runCommand(QList<QStringList>() << addConfigurationArguments(args));
jkobus's avatar
jkobus committed
317
}
318

319
class BranchDiffController : public BaseController
jkobus's avatar
jkobus committed
320
321
322
{
    Q_OBJECT
public:
323
324
325
326
327
    BranchDiffController(IDocument *document, const QString &dir,
                         const QString &branch) :
        BaseController(document, dir),
        m_branch(branch)
    { }
328

Orgad Shaneh's avatar
Orgad Shaneh committed
329
    void reload() override;
330

331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
private:
    const QString m_branch;
};

void BranchDiffController::reload()
{
    QStringList args;
    args << QLatin1String("diff") << addHeadWhenCommandInProgress() << m_branch;
    runCommand(QList<QStringList>() << addConfigurationArguments(args));
}

class ShowController : public BaseController
{
    Q_OBJECT
public:
    ShowController(IDocument *document, const QString &dir, const QString &id) :
        BaseController(document, dir),
        m_id(id),
        m_state(Idle)
    { }
351

Orgad Shaneh's avatar
Orgad Shaneh committed
352
353
    void reload() override;
    void processOutput(const QString &output) override;
jkobus's avatar
jkobus committed
354

jkobus's avatar
jkobus committed
355
private:
356
357
358
    const QString m_id;
    enum State { Idle, GettingDescription, GettingDiff };
    State m_state;
jkobus's avatar
jkobus committed
359
};
360

361
void ShowController::reload()
jkobus's avatar
jkobus committed
362
{
363
364
365
366
367
    QStringList args;
    args << QLatin1String("show") << QLatin1String("-s") << QLatin1String(noColorOption)
              << QLatin1String(decorateOption) << m_id;
    m_state = GettingDescription;
    runCommand(QList<QStringList>() << args, gitClient()->encoding(m_directory, "i18n.commitEncoding"));
368
369
}

370
void ShowController::processOutput(const QString &output)
371
{
372
373
374
    QTC_ASSERT(m_state != Idle, return);
    if (m_state == GettingDescription) {
        setDescription(gitClient()->extendedShowDescription(m_directory, output));
375

376
377
378
379
380
381
382
383
384
        QStringList args;
        args << QLatin1String("show") << QLatin1String("--format=format:") // omit header, already generated
             << QLatin1String("-M") << QLatin1String("-C") << QLatin1String(noColorOption)
             << QLatin1String(decorateOption) << m_id;
        m_state = GettingDiff;
        runCommand(QList<QStringList>() << addConfigurationArguments(args));
    } else if (m_state == GettingDiff) {
        m_state = Idle;
        processDiff(output);
jkobus's avatar
jkobus committed
385
    }
386
387
}

jkobus's avatar
jkobus committed
388
///////////////////////////////
389

hjk's avatar
hjk committed
390
class BaseGitDiffArgumentsWidget : public VcsBaseEditorParameterWidget
391
{
Friedemann Kleint's avatar
Friedemann Kleint committed
392
    Q_OBJECT
393

394
public:
395
    BaseGitDiffArgumentsWidget(VcsBaseClientSettings &settings, QWidget *parent = 0) :
396
        VcsBaseEditorParameterWidget(parent)
397
    {
jkobus's avatar
jkobus committed
398
399
400
401
        m_patienceButton = addToggleButton(
                    QLatin1String("--patience"),
                    tr("Patience"),
                    tr("Use the patience algorithm for calculating the differences."));
402
        mapSetting(m_patienceButton, settings.boolPointer(GitSettings::diffPatienceKey));
jkobus's avatar
jkobus committed
403
404
405
406
        m_ignoreWSButton = addToggleButton(
                    QLatin1String("--ignore-space-change"), tr("Ignore Whitespace"),
                    tr("Ignore whitespace only changes."));
        mapSetting(m_ignoreWSButton,
407
                   settings.boolPointer(GitSettings::ignoreSpaceChangesInDiffKey));
408
409
    }

410
protected:
411
412
    QToolButton *m_patienceButton;
    QToolButton *m_ignoreWSButton;
413
414
};

hjk's avatar
hjk committed
415
class GitBlameArgumentsWidget : public VcsBaseEditorParameterWidget
416
{
Friedemann Kleint's avatar
Friedemann Kleint committed
417
    Q_OBJECT
418

419
public:
420
    GitBlameArgumentsWidget(VcsBaseClientSettings &settings, QWidget *parent = 0) :
421
        VcsBaseEditorParameterWidget(parent)
422
    {
423
424
        mapSetting(addToggleButton(QString(), tr("Omit Date"),
                                   tr("Hide the date of a change from the output.")),
425
                   settings.boolPointer(GitSettings::omitAnnotationDateKey));
426
        mapSetting(addToggleButton(QLatin1String("-w"), tr("Ignore Whitespace"),
427
                                   tr("Ignore whitespace only changes.")),
428
                   settings.boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
429
430
431
    }
};

432
433
434
435
436
class GitLogArgumentsWidget : public BaseGitDiffArgumentsWidget
{
    Q_OBJECT

public:
437
    GitLogArgumentsWidget(VcsBaseClientSettings &settings, QWidget *parent = 0) :
438
        BaseGitDiffArgumentsWidget(settings, parent)
439
    {
440
        QToolButton *diffButton = addToggleButton(QLatin1String("--patch"), tr("Show Diff"),
441
                                              tr("Show difference."));
442
        mapSetting(diffButton, settings.boolPointer(GitSettings::logDiffKey));
443
444
        connect(diffButton, &QToolButton::toggled, m_patienceButton, &QToolButton::setVisible);
        connect(diffButton, &QToolButton::toggled, m_ignoreWSButton, &QToolButton::setVisible);
445
446
447
448
449
450
451
        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."));
452
        mapSetting(graphButton, settings.boolPointer(GitSettings::graphLogKey));
453
    }
454
455
};

Orgad Shaneh's avatar
Orgad Shaneh committed
456
457
458
459
class ConflictHandler : public QObject
{
    Q_OBJECT
public:
460
461
462
463
464
465
466
467
468
469
470
471
    static void attachToCommand(VcsCommand *command, const QString &abortCommand = QString()) {
        ConflictHandler *handler = new ConflictHandler(command->workingDirectory(), abortCommand);
        handler->setParent(command); // delete when command goes out of scope

        command->addFlags(VcsBasePlugin::ExpectRepoChanges);
        connect(command, &VcsCommand::output, handler, &ConflictHandler::readStdOut);
        connect(command, &VcsCommand::errorText, handler, &ConflictHandler::readStdErr);
    }

    static void handleResponse(const Utils::SynchronousProcessResponse &response,
                               const QString &workingDirectory,
                               const QString &abortCommand = QString())
Orgad Shaneh's avatar
Orgad Shaneh committed
472
    {
473
474
475
        ConflictHandler handler(workingDirectory, abortCommand);
        handler.readStdOut(response.stdOut);
        handler.readStdErr(response.stdErr);
Orgad Shaneh's avatar
Orgad Shaneh committed
476
477
    }

478
479
480
481
482
483
private:
    ConflictHandler(const QString &workingDirectory, const QString &abortCommand) :
          m_workingDirectory(workingDirectory),
          m_abortCommand(abortCommand)
    { }

Orgad Shaneh's avatar
Orgad Shaneh committed
484
485
    ~ConflictHandler()
    {
486
487
488
        // If interactive rebase editor window is closed, plugin is terminated
        // but referenced here when the command ends
        if (GitPlugin *plugin = GitPlugin::instance()) {
489
            GitClient *client = plugin->client();
490
491
492
493
            if (m_commit.isEmpty() && m_files.isEmpty()) {
                if (client->checkCommandInProgress(m_workingDirectory) == GitClient::NoCommand)
                    client->endStashScope(m_workingDirectory);
            } else {
494
                client->handleMergeConflicts(m_workingDirectory, m_commit, m_files, m_abortCommand);
495
            }
Orgad Shaneh's avatar
Orgad Shaneh committed
496
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
497
498
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
499
    void readStdOut(const QString &data)
Orgad Shaneh's avatar
Orgad Shaneh committed
500
501
    {
        static QRegExp patchFailedRE(QLatin1String("Patch failed at ([^\\n]*)"));
502
        static QRegExp conflictedFilesRE(QLatin1String("Merge conflict in ([^\\n]*)"));
Orgad Shaneh's avatar
Orgad Shaneh committed
503
504
        if (patchFailedRE.indexIn(data) != -1)
            m_commit = patchFailedRE.cap(1);
505
        int fileIndex = -1;
506
        while ((fileIndex = conflictedFilesRE.indexIn(data, fileIndex + 1)) != -1)
507
            m_files.append(conflictedFilesRE.cap(1));
Orgad Shaneh's avatar
Orgad Shaneh committed
508
509
510
511
512
513
514
515
516
517
    }

    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;
518
    QString m_abortCommand;
Orgad Shaneh's avatar
Orgad Shaneh committed
519
    QString m_commit;
520
    QStringList m_files;
Orgad Shaneh's avatar
Orgad Shaneh committed
521
522
};

hjk's avatar
hjk committed
523
class GitProgressParser : public ProgressParser
524
525
{
public:
526
    static void attachToCommand(VcsCommand *command)
527
    {
528
        command->setProgressParser(new GitProgressParser);
529
530
    }

531
532
533
534
private:
    GitProgressParser() : m_progressExp(QLatin1String("\\((\\d+)/(\\d+)\\)")) // e.g. Rebasing (7/42)
    { }

Orgad Shaneh's avatar
Orgad Shaneh committed
535
    void parseProgress(const QString &text) override
536
537
538
539
540
541
542
543
    {
        if (m_progressExp.lastIndexIn(text) != -1)
            setProgressAndMaximum(m_progressExp.cap(1).toInt(), m_progressExp.cap(2).toInt());
    }

    QRegExp m_progressExp;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
544
IEditor *locateEditor(const char *property, const QString &entry)
con's avatar
con committed
545
{
Orgad Shaneh's avatar
Orgad Shaneh committed
546
    foreach (IDocument *document, DocumentModel::openedDocuments())
547
        if (document->property(property).toString() == entry)
Orgad Shaneh's avatar
Orgad Shaneh committed
548
            return DocumentModel::editorsForDocument(document).first();
con's avatar
con committed
549
550
551
    return 0;
}

552
553
static inline QString msgRepositoryNotFound(const QString &dir)
{
Tobias Hunger's avatar
Tobias Hunger committed
554
    return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
555
556
557
558
}

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

Friedemann Kleint's avatar
Friedemann Kleint committed
562
563
564
565
566
static inline QString msgCannotLaunch(const QString &binary)
{
    return GitClient::tr("Cannot launch \"%1\".").arg(QDir::toNativeSeparators(binary));
}

567
568
569
570
571
static inline void msgCannotRun(const QString &message, QString *errorMessage)
{
    if (errorMessage)
        *errorMessage = message;
    else
572
        VcsOutputWindow::appendError(message);
573
574
575
576
577
578
}

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")
hjk's avatar
hjk committed
579
            .arg(QLatin1String("git ") + args.join(QLatin1Char(' ')),
580
                 QDir::toNativeSeparators(workingDirectory),
581
                 GitClient::commandOutputFromLocal8Bit(error));
582
583
584
585

    msgCannotRun(message, errorMessage);
}

586
// ---------------- GitClient
587
588
589

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

590
GitClient::GitClient() : VcsBase::VcsBaseClientImpl(this, new GitSettings),
591
    m_cachedGitVersion(0),
Orgad Shaneh's avatar
Orgad Shaneh committed
592
    m_disableEditor(false)
con's avatar
con committed
593
{
594
595
596
    m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
            .arg(QCoreApplication::applicationFilePath())
            .arg(QCoreApplication::applicationPid());
con's avatar
con committed
597
598
}

jkobus's avatar
jkobus committed
599
QString GitClient::findRepositoryForDirectory(const QString &dir) const
con's avatar
con committed
600
{
601
602
    if (dir.isEmpty() || dir.endsWith(QLatin1String("/.git"))
            || dir.contains(QLatin1String("/.git/"))) {
603
        return QString();
604
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
605
    QDir directory(dir);
606
607
608
    QString dotGit = QLatin1String(GIT_DIRECTORY);
    // QFileInfo is outside loop, because it is faster this way
    QFileInfo fileInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
609
    do {
610
611
612
613
614
615
616
        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
617
    } while (!directory.isRoot() && directory.cdUp());
618
    return QString();
con's avatar
con committed
619
620
}

Orgad Shaneh's avatar
Orgad Shaneh committed
621
QString GitClient::findGitDirForRepository(const QString &repositoryDir) const
Orgad Shaneh's avatar
Orgad Shaneh committed
622
{
623
624
625
626
    static QHash<QString, QString> repoDirCache;
    QString &res = repoDirCache[repositoryDir];
    if (!res.isEmpty())
        return res;
Petar Perisin's avatar
Petar Perisin committed
627
628
629

    synchronousRevParseCmd(repositoryDir, QLatin1String("--git-dir"), &res);

630
631
632
    if (!QDir(res).isAbsolute())
        res.prepend(repositoryDir + QLatin1Char('/'));
    return res;
Orgad Shaneh's avatar
Orgad Shaneh committed
633
634
}

635
636
637
638
639
bool GitClient::managesFile(const QString &workingDirectory, const QString &fileName) const
{
    QByteArray output;
    QStringList arguments;
    arguments << QLatin1String("ls-files") << QLatin1String("--error-unmatch") << fileName;
640
641
    return vcsFullySynchronousExec(workingDirectory, arguments, &output, 0,
                                   VcsBasePlugin::SuppressCommandLogging);
642
643
}

hjk's avatar
hjk committed
644
645
VcsBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
                                                      const QString &dynamicPropertyValue) const
646
{
hjk's avatar
hjk committed
647
    VcsBaseEditorWidget *rc = 0;
Orgad Shaneh's avatar
Orgad Shaneh committed
648
    IEditor *outputEditor = locateEditor(registerDynamicProperty, dynamicPropertyValue);
649
650
651
    if (!outputEditor)
        return 0;

652
    // Exists already
Orgad Shaneh's avatar
Orgad Shaneh committed
653
    EditorManager::activateEditor(outputEditor);
654
    outputEditor->document()->setContents(QByteArray()); // clear
655
    rc = VcsBaseEditor::getVcsBaseEditor(outputEditor);
656
657
658
659

    return rc;
}

660
661
662
663
664
665
666
667
668
669
670
QTextCodec *GitClient::codecFor(GitClient::CodecType codecType, const QString &source) const
{
    if (codecType == CodecSource) {
        return QFileInfo(source).isFile() ? VcsBaseEditor::getCodec(source)
                                          : encoding(source, "gui.encoding");
    }
    if (codecType == CodecLogOutput)
        return encoding(source, "i18n.logOutputEncoding");
    return 0;
}

Orgad Shaneh's avatar
Orgad Shaneh committed
671
void GitClient::slotChunkActionsRequested(QMenu *menu, bool isValid)
jkobus's avatar
jkobus committed
672
673
674
{
    menu->addSeparator();
    QAction *stageChunkAction = menu->addAction(tr("Stage Chunk"));
675
    connect(stageChunkAction, &QAction::triggered, this, &GitClient::slotStageChunk);
jkobus's avatar
jkobus committed
676
    QAction *unstageChunkAction = menu->addAction(tr("Unstage Chunk"));
677
    connect(unstageChunkAction, &QAction::triggered, this, &GitClient::slotUnstageChunk);
jkobus's avatar
jkobus committed
678

Tobias Hunger's avatar
Tobias Hunger committed
679
    m_contextController = qobject_cast<DiffEditorController *>(sender());
jkobus's avatar
jkobus committed
680

Orgad Shaneh's avatar
Orgad Shaneh committed
681
    if (!isValid || !m_contextController) {
jkobus's avatar
jkobus committed
682
683
684
685
686
687
688
        stageChunkAction->setEnabled(false);
        unstageChunkAction->setEnabled(false);
    }
}

void GitClient::slotStageChunk()
{
689
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
690
691
        return;

Orgad Shaneh's avatar
Orgad Shaneh committed
692
    const QString patch = m_contextController->makePatch(false, true);
jkobus's avatar
jkobus committed
693
694
695
696
697
698
699
700
    if (patch.isEmpty())
        return;

    stage(patch, false);
}

void GitClient::slotUnstageChunk()
{
701
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
702
703
        return;

Orgad Shaneh's avatar
Orgad Shaneh committed
704
    const QString patch = m_contextController->makePatch(true, true);
jkobus's avatar
jkobus committed
705
706
707
708
709
710
711
712
713
714
715
716
    if (patch.isEmpty())
        return;

    stage(patch, true);
}

void GitClient::stage(const QString &patch, bool revert)
{
    QTemporaryFile patchFile;
    if (!patchFile.open())
        return;

717
    const QString baseDir = m_contextController->baseDirectory();
Orgad Shaneh's avatar
Orgad Shaneh committed
718
    QTextCodec *codec = EditorManager::defaultTextCodec();
jkobus's avatar
jkobus committed
719
720
721
722
723
724
725
726
727
728
729
730
731
    const QByteArray patchData = codec
            ? codec->fromUnicode(patch) : patch.toLocal8Bit();
    patchFile.write(patchData);
    patchFile.close();

    QStringList args = QStringList() << QLatin1String("--cached");
    if (revert)
        args << QLatin1String("--reverse");
    QString errorMessage;
    if (synchronousApplyPatch(baseDir, patchFile.fileName(),
                              &errorMessage, args)) {
        if (errorMessage.isEmpty()) {
            if (revert)
732
                VcsOutputWindow::append(tr("Chunk successfully unstaged"));
jkobus's avatar
jkobus committed
733
            else
734
                VcsOutputWindow::append(tr("Chunk successfully staged"));
jkobus's avatar
jkobus committed
735
        } else {
736
            VcsOutputWindow::append(errorMessage);
jkobus's avatar
jkobus committed
737
        }
738
        m_contextController->requestReload();
jkobus's avatar
jkobus committed
739
    } else {
740
        VcsOutputWindow::appendError(errorMessage);
jkobus's avatar
jkobus committed
741
742
743
    }
}

744
745
746
747
void GitClient::requestReload(const QString &documentId, const QString &source,
                              const QString &title,
                              std::function<DiffEditorController *(IDocument *)> factory) const
{
748
    IDocument *document = DiffEditorController::findOrCreateDocument(documentId, title);
749
    QTC_ASSERT(document, return);
750
751
    DiffEditorController *controller = factory(document);
    QTC_ASSERT(controller, return);
752

753
754
755
756
    connect(controller, &DiffEditorController::chunkActionsRequested,
            this, &GitClient::slotChunkActionsRequested, Qt::DirectConnection);
    connect(controller, &DiffEditorController::requestInformationForCommit,
            this, &GitClient::branchesForCommit);
757
758
759
760
761
762

    VcsBasePlugin::setSource(document, source);
    EditorManager::activateEditorForDocument(document);
    controller->requestReload();
}

763
764
765
766
void GitClient::diffFiles(const QString &workingDirectory,
                          const QStringList &unstagedFileNames,
                          const QStringList &stagedFileNames) const
{
767
768
769
770
771
772
773
    requestReload(QLatin1String("Files:") + workingDirectory,
                  workingDirectory, tr("Git Diff Files"),
                  [this, workingDirectory, stagedFileNames, unstagedFileNames]
                  (IDocument *doc) -> DiffEditorController* {
                      return new FileListDiffController(doc, workingDirectory,
                                                        stagedFileNames, unstagedFileNames);
                  });
774
}
775

776
777
void GitClient::diffProject(const QString &workingDirectory, const QString &projectDirectory) const
{
778
779
780
781
782
783
784
    requestReload(QLatin1String("Project:") + workingDirectory,
                  workingDirectory, tr("Git Diff Project"),
                  [this, workingDirectory, projectDirectory]
                  (IDocument *doc) -> DiffEditorController* {
                      return new ProjectDiffController(doc, workingDirectory,
                                                       QStringList(projectDirectory));
                  });
785
}
jkobus's avatar
jkobus committed
786

787
788
void GitClient::diffRepository(const QString &workingDirectory) const
{
789
790
791
792
793
    requestReload(QLatin1String("Repository:") + workingDirectory,
                  workingDirectory, tr("Git Diff Repository"),
                  [this, workingDirectory](IDocument *doc) -> DiffEditorController* {
                      return new RepositoryDiffController(doc, workingDirectory);
                  });
con's avatar
con committed
794
795
}

796
void GitClient::diffFile(const QString &workingDirectory, const QString &fileName) const
con's avatar
con committed
797
{
hjk's avatar
hjk committed
798
    const QString title = tr("Git Diff \"%1\"").arg(fileName);
hjk's avatar
hjk committed
799
    const QString sourceFile = VcsBaseEditor::getSource(workingDirectory, fileName);
jkobus's avatar
jkobus committed
800
    const QString documentId = QLatin1String("File:") + sourceFile;
801
802
803
804
805
    requestReload(documentId, sourceFile, title,
                  [this, workingDirectory, fileName]
                  (IDocument *doc) -> DiffEditorController* {
                      return new FileDiffController(doc, workingDirectory, fileName);
                  });
con's avatar
con committed
806
807
}

808
void GitClient::diffBranch(const QString &workingDirectory,
jkobus's avatar
jkobus committed
809
                           const QString &branchName) const
810
{
hjk's avatar
hjk committed
811
    const QString title = tr("Git Diff Branch \"%1\"").arg(branchName);
jkobus's avatar
jkobus committed
812
    const QString documentId = QLatin1String("Branch:") + branchName;
813
814
815
816
817
    requestReload(documentId, workingDirectory, title,
                               [this, workingDirectory, branchName]
                               (IDocument *doc) -> DiffEditorController* {
                                   return new BranchDiffController(doc, workingDirectory, branchName);
                               });
818
819
}

jkobus's avatar
jkobus committed
820
821
void GitClient::merge(const QString &workingDirectory,
                      const QStringList &unmergedFileNames)
822
{
Tobias Hunger's avatar
Tobias Hunger committed
823
    auto mergeTool = new MergeTool(this);
824
825
826
827
    if (!mergeTool->start(workingDirectory, unmergedFileNames))
        delete mergeTool;
}

con's avatar
con committed
828
829
void GitClient::status(const QString &workingDirectory)
{
830
831
    QStringList statusArgs;
    statusArgs << QLatin1String("status") << QLatin1String("-u");
832
    VcsOutputWindow::setRepository(workingDirectory);
833
    VcsCommand *command = vcsExec(workingDirectory, statusArgs, 0, true);
834
    connect(command, &VcsCommand::finished, VcsOutputWindow::instance(), &VcsOutputWindow::clearRepository,
835
            Qt::QueuedConnection);
con's avatar
con committed
836
837
}

838
void GitClient::log(const QString &workingDirectory, const QString &fileName,
839
                    bool enableAnnotationContextMenu, const QStringList &args)
con's avatar
con committed
840
{
841
842
843
844
845
846
847
    QString msgArg;
    if (!fileName.isEmpty())
        msgArg = fileName;
    else if (!args.isEmpty())
        msgArg = args.first();
    else
        msgArg = workingDirectory;
848
    const QString title = tr("Git Log \"%1\"").arg(msgArg);
Orgad Shaneh's avatar
Orgad Shaneh committed
849
    const Id editorId = Git::Constants::GIT_LOG_EDITOR_ID;
850
    const QString sourceFile = VcsBaseEditor::getSource(workingDirectory, fileName);
851
    VcsBaseEditorWidget *editor = findExistingVCSEditor("logTitle", msgArg);
852
853
    if (!editor) {
        auto *argWidget = new GitLogArgumentsWidget(settings());
854
855
        connect(argWidget, &VcsBaseEditorParameterWidget::commandExecutionRequested,
                [=]() { this->log(workingDirectory, fileName, enableAnnotationContextMenu, args); });
856
857
        editor = createVcsEditor(editorId, title, sourceFile, codecFor(CodecLogOutput),
                                 "logTitle", msgArg);
858
        editor->setConfigurationWidget(argWidget);
859
    }
860
    editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);
861
    editor->setWorkingDirectory(workingDirectory);
862

863
    QStringList arguments;
864
865
    arguments << QLatin1String("log") << QLatin1String(noColorOption)
              << QLatin1String(decorateOption);
866

867
    int logCount = settings().intValue(GitSettings::logCountKey);
868
869
    if (logCount > 0)
         arguments << QLatin1String("-n") << QString::number(logCount);
870

871
    auto *argWidget = editor->configurationWidget();
872
    argWidget->setBaseArguments(args);
873
874
875
876
    QStringList userArgs = argWidget->arguments();

    arguments.append(userArgs);

877
    if (!fileName.isEmpty())
Orgad Shaneh's avatar
Orgad Shaneh committed
878
        arguments << QLatin1String("--follow") << QLatin1String("--") << fileName;
con's avatar
con committed
879

880
    vcsExec(workingDirectory, arguments, editor);
con's avatar
con committed
881
882
}

Orgad Shaneh's avatar
Orgad Shaneh committed
883
884
885
void GitClient::reflog(const QString &workingDirectory)
{
    const QString title = tr("Git Reflog \"%1\"").arg(workingDirectory);
Orgad Shaneh's avatar
Orgad Shaneh committed
886
    const Id editorId = Git::Constants::GIT_LOG_EDITOR_ID;
hjk's avatar
hjk committed
887
    VcsBaseEditorWidget *editor = findExistingVCSEditor("reflogRepository", workingDirectory);
Orgad Shaneh's avatar
Orgad Shaneh committed
888
    if (!editor) {
889
        editor = createVcsEditor(editorId, title, workingDirectory, codecFor(CodecLogOutput),
890
                                 "reflogRepository", workingDirectory);
Orgad Shaneh's avatar
Orgad Shaneh committed
891
    }
892
    editor->setWorkingDirectory(workingDirectory);
Orgad Shaneh's avatar
Orgad Shaneh committed
893
894
895