gitclient.cpp 138 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>
hjk's avatar
hjk committed
56
#include <vcsbase/vcscommand.h>
hjk's avatar
hjk committed
57
#include <vcsbase/vcsbaseeditor.h>
58
#include <vcsbase/vcsbaseeditorparameterwidget.h>
59
#include <vcsbase/vcsoutputwindow.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>
jkobus's avatar
jkobus committed
66 67
#include <diffeditor/diffeditorreloader.h>
#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
#include <QMessageBox>
78
#include <QPushButton>
79 80
#include <QToolButton>
#include <QTextCodec>
con's avatar
con committed
81

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

Orgad Shaneh's avatar
Orgad Shaneh committed
89
using namespace Core;
hjk's avatar
hjk committed
90
using namespace Utils;
91
using namespace VcsBase;
Orgad Shaneh's avatar
Orgad Shaneh committed
92

93 94 95
namespace Git {
namespace Internal {

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

102 103 104 105 106
class GitDiffHandler : public QObject
{
    Q_OBJECT

public:
107
    GitDiffHandler(DiffEditor::DiffEditorController *controller,
jkobus's avatar
jkobus committed
108
                   const QString &workingDirectory);
109 110 111

    // index -> working tree
    void diffFile(const QString &fileName);
112 113
    // stagedFileNames:   HEAD -> index
    // unstagedFileNames: index -> working tree
jkobus's avatar
jkobus committed
114 115
    void diffFiles(const QStringList &stagedFileNames,
                   const QStringList &unstagedFileNames);
116 117 118 119
    // index -> working tree
    void diffProjects(const QStringList &projectPaths);
    // index -> working tree
    void diffRepository();
120 121 122 123
    // branch HEAD -> working tree
    void diffBranch(const QString &branchName);
    // id^ -> id
    void show(const QString &id);
124 125

private slots:
Orgad Shaneh's avatar
Orgad Shaneh committed
126
    void slotShowDescriptionReceived(const QString &data);
127
    void slotTextualDiffOutputReceived(const QString &contents);
128 129

private:
jkobus's avatar
jkobus committed
130
    void postCollectShowDescription(const QString &id);
131 132 133 134 135
    void postCollectTextualDiffOutputUsingDiffCommand(const QStringList &arguments);
    void postCollectTextualDiffOutputUsingDiffCommand(const QList<QStringList> &argumentsList);
    void postCollectTextualDiffOutputUsingShowCommand(const QStringList &arguments);
    void postCollectTextualDiffOutput(const QString &gitCommand,
                                      const QList<QStringList> &argumentsList);
hjk's avatar
hjk committed
136
    void addJob(VcsCommand *command,
137 138
                const QString &gitCommand,
                const QStringList &arguments);
139
    QStringList addHeadWhenCommandInProgress() const;
jkobus's avatar
jkobus committed
140 141
    int timeout() const;
    QProcessEnvironment processEnvironment() const;
hjk's avatar
hjk committed
142
    FileName gitPath() const;
143

144
    QPointer<DiffEditor::DiffEditorController> m_controller;
145
    const QString m_workingDirectory;
jkobus's avatar
jkobus committed
146
    GitClient *m_gitClient;
147 148
    const QString m_waitMessage;

jkobus's avatar
jkobus committed
149
    QString m_id;
150 151
};

152
GitDiffHandler::GitDiffHandler(DiffEditor::DiffEditorController *controller,
jkobus's avatar
jkobus committed
153
               const QString &workingDirectory)
154
    : m_controller(controller),
155
      m_workingDirectory(workingDirectory),
jkobus's avatar
jkobus committed
156
      m_gitClient(GitPlugin::instance()->gitClient()),
157 158 159 160 161 162
      m_waitMessage(tr("Waiting for data..."))
{
}

void GitDiffHandler::diffFile(const QString &fileName)
{
163 164 165
    postCollectTextualDiffOutputUsingDiffCommand(addHeadWhenCommandInProgress()
                                                 << QLatin1String("--")
                                                 << fileName);
166 167
}

jkobus's avatar
jkobus committed
168 169
void GitDiffHandler::diffFiles(const QStringList &stagedFileNames,
                               const QStringList &unstagedFileNames)
170
{
jkobus's avatar
jkobus committed
171
    QList<QStringList> arguments;
172

jkobus's avatar
jkobus committed
173 174 175 176 177
    QStringList stagedArguments;
    stagedArguments << QLatin1String("--cached");
    stagedArguments << QLatin1String("--");
    stagedArguments << stagedFileNames;
    arguments << stagedArguments;
178

jkobus's avatar
jkobus committed
179
    if (!unstagedFileNames.isEmpty()) {
180
        QStringList unstagedArguments = addHeadWhenCommandInProgress();
jkobus's avatar
jkobus committed
181 182 183 184
        unstagedArguments << QLatin1String("--");
        unstagedArguments << unstagedFileNames;
        arguments << unstagedArguments;
    }
185

186
    postCollectTextualDiffOutputUsingDiffCommand(arguments);
187 188 189 190
}

void GitDiffHandler::diffProjects(const QStringList &projectPaths)
{
191 192 193
    postCollectTextualDiffOutputUsingDiffCommand(addHeadWhenCommandInProgress()
                                                 << QLatin1String("--")
                                                 << projectPaths);
194 195 196 197
}

void GitDiffHandler::diffRepository()
{
198
    postCollectTextualDiffOutputUsingDiffCommand(addHeadWhenCommandInProgress());
199 200
}

201 202
void GitDiffHandler::diffBranch(const QString &branchName)
{
203 204
    postCollectTextualDiffOutputUsingDiffCommand(addHeadWhenCommandInProgress()
                                                 << branchName);
205 206 207 208
}

void GitDiffHandler::show(const QString &id)
{
jkobus's avatar
jkobus committed
209 210
    m_id = id;
    postCollectShowDescription(id);
211 212
}

jkobus's avatar
jkobus committed
213
void GitDiffHandler::postCollectShowDescription(const QString &id)
214
{
215
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
216
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
217
        return;
jkobus's avatar
jkobus committed
218 219
    }

Robert Loehning's avatar
Robert Loehning committed
220
    m_controller->requestSaveState();
221
    m_controller->clear(m_waitMessage);
hjk's avatar
hjk committed
222
    VcsCommand *command = new VcsCommand(gitPath(), m_workingDirectory, processEnvironment());
jkobus's avatar
jkobus committed
223 224 225 226
    command->setCodec(m_gitClient->encoding(m_workingDirectory,
                                            "i18n.commitEncoding"));
    connect(command, SIGNAL(output(QString)),
            this, SLOT(slotShowDescriptionReceived(QString)));
227
    QStringList arguments;
jkobus's avatar
jkobus committed
228 229 230 231 232 233
    arguments << QLatin1String("show")
              << QLatin1String("-s")
              << QLatin1String(noColorOption)
              << QLatin1String(decorateOption)
              << id;
    command->addJob(arguments, timeout());
234 235 236
    command->execute();
}

Orgad Shaneh's avatar
Orgad Shaneh committed
237
void GitDiffHandler::slotShowDescriptionReceived(const QString &description)
238
{
239
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
240
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
241
        return;
jkobus's avatar
jkobus committed
242
    }
Jarek Kobus's avatar
Jarek Kobus committed
243

244 245 246 247 248
    postCollectTextualDiffOutputUsingShowCommand(QStringList()
              << QLatin1String("--format=format:") // omit header, already generated
              << QLatin1String(noColorOption)
              << QLatin1String(decorateOption)
              << m_id);
249

jkobus's avatar
jkobus committed
250
    // need to be called after postCollectDiffOutput(), since it clears the description
251
    m_controller->setDescription(
jkobus's avatar
jkobus committed
252 253
                m_gitClient->extendedShowDescription(m_workingDirectory,
                                                     description));
254 255
}

hjk's avatar
hjk committed
256
void GitDiffHandler::addJob(VcsCommand *command,
257 258
                            const QString &gitCommand,
                            const QStringList &arguments)
jkobus's avatar
jkobus committed
259 260
{
    QStringList args;
261 262 263
    args << gitCommand;
    args << QLatin1String("-m"); // show diff agains parents instead of merge commits
    args << QLatin1String("--first-parent"); // show only first parent
264
    if (m_controller->isIgnoreWhitespace())
jkobus's avatar
jkobus committed
265 266
        args << QLatin1String("--ignore-space-change");
    args << QLatin1String("--unified=") + QString::number(
267
                m_controller->contextLinesNumber());
jkobus's avatar
jkobus committed
268 269 270 271
    args << arguments;
    command->addJob(args, timeout());
}

272
QStringList GitDiffHandler::addHeadWhenCommandInProgress() const
jkobus's avatar
jkobus committed
273
{
274 275 276 277 278 279 280 281
    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.
    GitClient::CommandInProgress commandInProgress = m_gitClient->checkCommandInProgress(m_workingDirectory);
    if (commandInProgress != GitClient::NoCommand)
        args << QLatin1String(HEAD);
    return args;
jkobus's avatar
jkobus committed
282 283
}

284 285 286 287 288 289 290 291 292 293 294
void GitDiffHandler::postCollectTextualDiffOutputUsingDiffCommand(const QStringList &arguments)
{
    postCollectTextualDiffOutputUsingDiffCommand(QList<QStringList>() << arguments);
}

void GitDiffHandler::postCollectTextualDiffOutputUsingDiffCommand(const QList<QStringList> &argumentsList)
{
    postCollectTextualDiffOutput(QLatin1String("diff"), argumentsList);
}

void GitDiffHandler::postCollectTextualDiffOutputUsingShowCommand(const QStringList &arguments)
jkobus's avatar
jkobus committed
295
{
296
    postCollectTextualDiffOutput(QLatin1String("show"), QList<QStringList>() << arguments);
jkobus's avatar
jkobus committed
297 298
}

299
void GitDiffHandler::postCollectTextualDiffOutput(const QString &gitCommand, const QList<QStringList> &argumentsList)
300
{
301
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
302
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
303
        return;
jkobus's avatar
jkobus committed
304 305
    }

Robert Loehning's avatar
Robert Loehning committed
306
    m_controller->requestSaveState();
307
    m_controller->clear(m_waitMessage);
hjk's avatar
hjk committed
308
    VcsCommand *command = new VcsCommand(gitPath(), m_workingDirectory, processEnvironment());
Orgad Shaneh's avatar
Orgad Shaneh committed
309
    command->setCodec(EditorManager::defaultTextCodec());
jkobus's avatar
jkobus committed
310
    connect(command, SIGNAL(output(QString)),
311
            this, SLOT(slotTextualDiffOutputReceived(QString)));
312
    command->addFlags(diffExecutionFlags());
jkobus's avatar
jkobus committed
313 314

    for (int i = 0; i < argumentsList.count(); i++)
315
        addJob(command, gitCommand, argumentsList.at(i));
jkobus's avatar
jkobus committed
316

317 318 319
    command->execute();
}

320
void GitDiffHandler::slotTextualDiffOutputReceived(const QString &contents)
321
{
322
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
323
        deleteLater();
324
        return;
jkobus's avatar
jkobus committed
325
    }
326

jkobus's avatar
jkobus committed
327 328 329
    bool ok;
    QList<DiffEditor::FileData> fileDataList
            = DiffEditor::DiffUtils::readPatch(
330 331
                contents, m_controller->isIgnoreWhitespace(), &ok);
    m_controller->setDiffFiles(fileDataList, m_workingDirectory);
Robert Loehning's avatar
Robert Loehning committed
332
    m_controller->requestRestoreState();
jkobus's avatar
jkobus committed
333
    deleteLater();
334 335
}

jkobus's avatar
jkobus committed
336
int GitDiffHandler::timeout() const
337
{
jkobus's avatar
jkobus committed
338
    return m_gitClient->settings()->intValue(GitSettings::timeoutKey);
339
}
340

jkobus's avatar
jkobus committed
341
QProcessEnvironment GitDiffHandler::processEnvironment() const
342
{
jkobus's avatar
jkobus committed
343 344
    return m_gitClient->processEnvironment();
}
345

hjk's avatar
hjk committed
346
FileName GitDiffHandler::gitPath() const
jkobus's avatar
jkobus committed
347
{
348
    return m_gitClient->gitExecutable();
jkobus's avatar
jkobus committed
349
}
350

jkobus's avatar
jkobus committed
351
/////////////////////////////////////
352

jkobus's avatar
jkobus committed
353 354 355 356 357 358 359 360 361 362 363 364
class GitDiffEditorReloader : public DiffEditor::DiffEditorReloader
{
    Q_OBJECT
public:
    enum DiffType {
        DiffRepository,
        DiffFile,
        DiffFileList,
        DiffProjectList,
        DiffBranch,
        DiffShow
    };
365

jkobus's avatar
jkobus committed
366 367 368
    GitDiffEditorReloader(QObject *parent);
    void setWorkingDirectory(const QString &workingDir) {
        m_workingDirectory = workingDir;
369
    }
jkobus's avatar
jkobus committed
370 371 372 373 374 375
    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;
jkobus's avatar
jkobus committed
376
    }
jkobus's avatar
jkobus committed
377 378 379 380 381 382 383 384 385
    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;
386
    }
387 388


jkobus's avatar
jkobus committed
389 390
protected:
    void reload();
jkobus's avatar
jkobus committed
391

jkobus's avatar
jkobus committed
392 393
private:
    GitClient *m_gitClient;
394

jkobus's avatar
jkobus committed
395 396 397 398 399 400 401 402 403 404
    QString m_workingDirectory;
    DiffType m_diffType;
    QString m_fileName;
    QStringList m_stagedFiles;
    QStringList m_unstagedFiles;
    QStringList m_projectFiles;
    QString m_branchName;
    QString m_id;
    QString m_displayName;
};
405

jkobus's avatar
jkobus committed
406 407 408 409
GitDiffEditorReloader::GitDiffEditorReloader(QObject *parent)
    : DiffEditorReloader(parent),
      m_gitClient(GitPlugin::instance()->gitClient())
{
410 411
}

jkobus's avatar
jkobus committed
412
void GitDiffEditorReloader::reload()
413
{
414
    GitDiffHandler *handler = new GitDiffHandler(controller(),
415
                                                 m_workingDirectory);
jkobus's avatar
jkobus committed
416
    connect(handler, SIGNAL(destroyed()), this, SLOT(reloadFinished()));
417

jkobus's avatar
jkobus committed
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
    switch (m_diffType) {
    case DiffRepository:
        handler->diffRepository();
        break;
    case DiffFile:
        handler->diffFile(m_fileName);
        break;
    case DiffFileList:
        handler->diffFiles(m_stagedFiles, m_unstagedFiles);
        break;
    case DiffProjectList:
        handler->diffProjects(m_projectFiles);
        break;
    case DiffBranch:
        handler->diffBranch(m_branchName);
        break;
    case DiffShow:
        handler->show(m_id);
        break;
    default:
        break;
    }
440 441
}

jkobus's avatar
jkobus committed
442
///////////////////////////////
443

hjk's avatar
hjk committed
444
class BaseGitDiffArgumentsWidget : public VcsBaseEditorParameterWidget
445
{
Friedemann Kleint's avatar
Friedemann Kleint committed
446
    Q_OBJECT
447

448
public:
449
    BaseGitDiffArgumentsWidget(GitClient *client, const QString &directory,
450
                               const QStringList &args) :
451
        m_workingDirectory(directory),
452
        m_client(client)
453
    {
454 455
        QTC_ASSERT(!directory.isEmpty(), return);
        QTC_ASSERT(m_client, return);
456

jkobus's avatar
jkobus committed
457 458 459 460 461 462 463 464 465 466 467
        m_patienceButton = addToggleButton(
                    QLatin1String("--patience"),
                    tr("Patience"),
                    tr("Use the patience algorithm for calculating the differences."));
        mapSetting(m_patienceButton, client->settings()->boolPointer(
                       GitSettings::diffPatienceKey));
        m_ignoreWSButton = addToggleButton(
                    QLatin1String("--ignore-space-change"), tr("Ignore Whitespace"),
                    tr("Ignore whitespace only changes."));
        mapSetting(m_ignoreWSButton,
                   m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInDiffKey));
468 469

        setBaseArguments(args);
470 471
    }

472
protected:
473 474
    QString m_workingDirectory;
    GitClient *m_client;
475 476
    QToolButton *m_patienceButton;
    QToolButton *m_ignoreWSButton;
477 478
};

hjk's avatar
hjk committed
479
class GitBlameArgumentsWidget : public VcsBaseEditorParameterWidget
480
{
Friedemann Kleint's avatar
Friedemann Kleint committed
481
    Q_OBJECT
482

483
public:
484 485 486 487
    GitBlameArgumentsWidget(Git::Internal::GitClient *client,
                            const QString &directory,
                            const QStringList &args,
                            const QString &revision, const QString &fileName) :
488
        m_editor(0),
489 490
        m_client(client),
        m_workingDirectory(directory),
491 492 493
        m_revision(revision),
        m_fileName(fileName)
    {
494 495 496
        mapSetting(addToggleButton(QString(), tr("Omit Date"),
                                   tr("Hide the date of a change from the output.")),
                   m_client->settings()->boolPointer(GitSettings::omitAnnotationDateKey));
497
        mapSetting(addToggleButton(QLatin1String("-w"), tr("Ignore Whitespace"),
498 499
                                   tr("Ignore whitespace only changes.")),
                   m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
500 501

        setBaseArguments(args);
502 503
    }

hjk's avatar
hjk committed
504
    void setEditor(VcsBaseEditorWidget *editor)
505
    {
506
        QTC_ASSERT(editor, return);
507 508 509
        m_editor = editor;
    }

510
    void executeCommand()
511
    {
hjk's avatar
hjk committed
512
        int line = VcsBaseEditor::lineNumberOfCurrentEditor();
513
        m_client->blame(m_workingDirectory, baseArguments(), m_fileName, m_revision, line);
514 515 516
    }

private:
hjk's avatar
hjk committed
517
    VcsBaseEditorWidget *m_editor;
518 519
    GitClient *m_client;
    QString m_workingDirectory;
520 521 522 523
    QString m_revision;
    QString m_fileName;
};

524 525 526 527 528 529 530 531 532
class GitLogArgumentsWidget : public BaseGitDiffArgumentsWidget
{
    Q_OBJECT

public:
    GitLogArgumentsWidget(Git::Internal::GitClient *client,
                          const QString &directory,
                          bool enableAnnotationContextMenu,
                          const QStringList &args,
533
                          const QString &fileName) :
534 535 536
        BaseGitDiffArgumentsWidget(client, directory, args),
        m_client(client),
        m_workingDirectory(directory),
537
        m_enableAnnotationContextMenu(enableAnnotationContextMenu)
538
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
539
        QTC_ASSERT(!directory.isEmpty(), return);
540
        QToolButton *diffButton = addToggleButton(QLatin1String("--patch"), tr("Show Diff"),
541
                                              tr("Show difference."));
542 543 544 545 546 547 548 549 550 551 552
        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));
553
        setFileName(fileName);
554 555
    }

556
    void setFileName(const QString &fileNames)
557
    {
558
        m_fileName = fileNames;
559 560 561 562
    }

    void executeCommand()
    {
563
        m_client->log(m_workingDirectory, m_fileName, m_enableAnnotationContextMenu, baseArguments());
564 565 566 567 568 569
    }

private:
    GitClient *m_client;
    QString m_workingDirectory;
    bool m_enableAnnotationContextMenu;
570
    QString m_fileName;
571 572
};

Orgad Shaneh's avatar
Orgad Shaneh committed
573 574 575 576
class ConflictHandler : public QObject
{
    Q_OBJECT
public:
hjk's avatar
hjk committed
577
    ConflictHandler(VcsCommand *parentCommand,
Orgad Shaneh's avatar
Orgad Shaneh committed
578
                    const QString &workingDirectory,
579
                    const QString &command = QString())
Orgad Shaneh's avatar
Orgad Shaneh committed
580
        : QObject(parentCommand),
Orgad Shaneh's avatar
Orgad Shaneh committed
581 582 583
          m_workingDirectory(workingDirectory),
          m_command(command)
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
584
        if (parentCommand) {
Orgad Shaneh's avatar
Orgad Shaneh committed
585
            parentCommand->addFlags(VcsBasePlugin::ExpectRepoChanges);
586
            connect(parentCommand, SIGNAL(output(QString)), this, SLOT(readStdOut(QString)));
Orgad Shaneh's avatar
Orgad Shaneh committed
587 588
            connect(parentCommand, SIGNAL(errorText(QString)), this, SLOT(readStdErr(QString)));
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
589 590 591 592
    }

    ~ConflictHandler()
    {
593 594 595 596 597 598 599 600 601 602
        // 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
603
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
604 605
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
606 607
public slots:
    void readStdOut(const QString &data)
Orgad Shaneh's avatar
Orgad Shaneh committed
608 609
    {
        static QRegExp patchFailedRE(QLatin1String("Patch failed at ([^\\n]*)"));
610
        static QRegExp conflictedFilesRE(QLatin1String("Merge conflict in ([^\\n]*)"));
Orgad Shaneh's avatar
Orgad Shaneh committed
611 612
        if (patchFailedRE.indexIn(data) != -1)
            m_commit = patchFailedRE.cap(1);
613 614 615 616
        int fileIndex = -1;
        while ((fileIndex = conflictedFilesRE.indexIn(data, fileIndex + 1)) != -1) {
            m_files.append(conflictedFilesRE.cap(1));
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
617 618 619 620 621 622 623 624 625 626 627 628
    }

    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;
629
    QStringList m_files;
Orgad Shaneh's avatar
Orgad Shaneh committed
630 631
};

hjk's avatar
hjk committed
632
class GitProgressParser : public ProgressParser
633 634
{
public:
hjk's avatar
hjk committed
635
    GitProgressParser() :
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
        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
652

Orgad Shaneh's avatar
Orgad Shaneh committed
653
IEditor *locateEditor(const char *property, const QString &entry)
con's avatar
con committed
654
{
Orgad Shaneh's avatar
Orgad Shaneh committed
655
    foreach (IDocument *document, DocumentModel::openedDocuments())
656
        if (document->property(property).toString() == entry)
Orgad Shaneh's avatar
Orgad Shaneh committed
657
            return DocumentModel::editorsForDocument(document).first();
con's avatar
con committed
658 659 660
    return 0;
}

661 662 663
// Return converted command output, remove '\r' read on Windows
static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
{
hjk's avatar
hjk committed
664
    return SynchronousProcess::normalizeNewlines(QString::fromLocal8Bit(a));
665 666 667 668 669 670 671 672 673 674 675 676 677 678
}

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

679 680
static inline QString msgRepositoryNotFound(const QString &dir)
{
Tobias Hunger's avatar
Tobias Hunger committed
681
    return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
682 683 684 685
}

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

Friedemann Kleint's avatar
Friedemann Kleint committed
689 690 691 692 693
static inline QString msgCannotLaunch(const QString &binary)
{
    return GitClient::tr("Cannot launch \"%1\".").arg(QDir::toNativeSeparators(binary));
}

694 695
static inline QString currentDocumentPath()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
696
    if (IDocument *document= EditorManager::currentDocument())
697
        return QFileInfo(document->filePath()).path();
698
    return QString();
699 700
}

Orgad Shaneh's avatar
Orgad Shaneh committed
701 702 703 704 705 706
static inline QStringList statusArguments()
{
    return QStringList() << QLatin1String("-c") << QLatin1String("color.status=false")
                         << QLatin1String("status");
}

707 708 709 710 711
static inline void msgCannotRun(const QString &message, QString *errorMessage)
{
    if (errorMessage)
        *errorMessage = message;
    else
712
        VcsOutputWindow::appendError(message);
713 714 715 716 717 718
}

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
719
            .arg(QLatin1String("git ") + args.join(QLatin1Char(' ')),
720 721 722 723 724 725
                 QDir::toNativeSeparators(workingDirectory),
                 commandOutputFromLocal8Bit(error));

    msgCannotRun(message, errorMessage);
}

726
// ---------------- GitClient
727 728 729

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

730 731
GitClient::GitClient(GitSettings *settings) :
    m_cachedGitVersion(0),
Tobias Hunger's avatar
Tobias Hunger committed
732
    m_msgWait(tr("Waiting for data...")),
733
    m_settings(settings),
jkobus's avatar
jkobus committed
734 735 736
    m_disableEditor(false),
    m_contextDiffFileIndex(-1),
    m_contextChunkIndex(-1)
con's avatar
con committed
737
{
738
    QTC_CHECK(settings);
Orgad Shaneh's avatar
Orgad Shaneh committed
739
    connect(ICore::instance(), SIGNAL(saveSettingsRequested()), this, SLOT(saveSettings()));
740 741 742
    m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
            .arg(QCoreApplication::applicationFilePath())
            .arg(QCoreApplication::applicationPid());
con's avatar
con committed
743 744 745 746 747 748
}

GitClient::~GitClient()
{
}

jkobus's avatar
jkobus committed
749
QString GitClient::findRepositoryForDirectory(const QString &dir) const
con's avatar
con committed
750
{
751 752
    if (dir.isEmpty() || dir.endsWith(QLatin1String("/.git"))
            || dir.contains(QLatin1String("/.git/"))) {
753
        return QString();
754
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
755
    QDir directory(dir);
756 757 758
    QString dotGit = QLatin1String(GIT_DIRECTORY);
    // QFileInfo is outside loop, because it is faster this way
    QFileInfo fileInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
759
    do {
760 761 762 763 764 765 766
        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
767
    } while (!directory.isRoot() && directory.cdUp());
768
    return QString();
con's avatar
con committed
769 770
}

Orgad Shaneh's avatar
Orgad Shaneh committed
771
QString GitClient::findGitDirForRepository(const QString &repositoryDir) const
Orgad Shaneh's avatar
Orgad Shaneh committed
772
{
773 774 775 776
    static QHash<QString, QString> repoDirCache;
    QString &res = repoDirCache[repositoryDir];
    if (!res.isEmpty())
        return res;
Petar Perisin's avatar
Petar Perisin committed
777 778 779

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

780 781 782
    if (!QDir(res).isAbsolute())
        res.prepend(repositoryDir + QLatin1Char('/'));
    return res;
Orgad Shaneh's avatar
Orgad Shaneh committed
783 784
}

785 786 787 788 789 790 791 792 793
bool GitClient::managesFile(const QString &workingDirectory, const QString &fileName) const
{
    QByteArray output;
    QStringList arguments;
    arguments << QLatin1String("ls-files") << QLatin1String("--error-unmatch") << fileName;
    return fullySynchronousGit(workingDirectory, arguments, &output, 0,
                               VcsBasePlugin::SuppressCommandLogging);
}

hjk's avatar
hjk committed
794 795
VcsBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
                                                      const QString &dynamicPropertyValue) const
796
{
hjk's avatar
hjk committed
797
    VcsBaseEditorWidget *rc = 0;
Orgad Shaneh's avatar
Orgad Shaneh committed
798
    IEditor *outputEditor = locateEditor(registerDynamicProperty, dynamicPropertyValue);
799 800 801
    if (!outputEditor)
        return 0;

802
    // Exists already
Orgad Shaneh's avatar
Orgad Shaneh committed
803
    EditorManager::activateEditor(outputEditor);
804
    outputEditor->document()->setContents(m_msgWait.toUtf8());
805
    rc = VcsBaseEditor::getVcsBaseEditor(outputEditor);
806 807 808 809

    return rc;
}

810 811 812 813 814 815 816 817 818 819 820 821 822 823 824
GitDiffEditorReloader *GitClient::findOrCreateDiffEditor(const QString &documentId,
                                                         const QString &source,
                                                         const QString &title,
                                                         const QString &workingDirectory) const
{
    DiffEditor::DiffEditorController *controller = 0;
    GitDiffEditorReloader *reloader = 0;
    DiffEditor::DiffEditorDocument *diffEditorDocument = DiffEditor::DiffEditorManager::find(documentId);
    if (diffEditorDocument) {
        controller = diffEditorDocument->controller();
        reloader = static_cast<GitDiffEditorReloader *>(controller->reloader());
    } else {
        diffEditorDocument = DiffEditor::DiffEditorManager::findOrCreate(documentId, title);
        QTC_ASSERT(diffEditorDocument, return 0);
        controller = diffEditorDocument->controller();
jkobus's avatar
jkobus committed
825

826 827 828 829
        connect(controller, SIGNAL(chunkActionsRequested(QMenu*,int,int)),
                this, SLOT(slotChunkActionsRequested(QMenu*,int,int)), Qt::DirectConnection);
        connect(controller, SIGNAL(expandBranchesRequested(QString)),
                this, SLOT(branchesForCommit(QString)));
jkobus's avatar
jkobus committed
830

831 832 833 834 835 836 837 838 839
        reloader = new GitDiffEditorReloader(controller);
        controller->setReloader(reloader);

        reloader->setWorkingDirectory(workingDirectory);
    }

    VcsBasePlugin::setSource(diffEditorDocument, source);
    EditorManager::activateEditorForDocument(diffEditorDocument);
    return reloader;
840
}
hjk's avatar
hjk committed
841

jkobus's avatar
jkobus committed
842 843 844 845 846 847 848 849 850 851
void GitClient::slotChunkActionsRequested(QMenu *menu, int diffFileIndex, int chunkIndex)
{
    menu->addSeparator();
    QAction *stageChunkAction = menu->addAction(tr("Stage Chunk"));
    connect(stageChunkAction, SIGNAL(triggered()), this, SLOT(slotStageChunk()));
    QAction *unstageChunkAction = menu->addAction(tr("Unstage Chunk"));
    connect(unstageChunkAction, SIGNAL(triggered()), this, SLOT(slotUnstageChunk()));

    m_contextDiffFileIndex = diffFileIndex;
    m_contextChunkIndex = chunkIndex;
852
    m_contextController = qobject_cast<DiffEditor::DiffEditorController *>(sender());
jkobus's avatar
jkobus committed
853

854
    if (m_contextDiffFileIndex < 0 || m_contextChunkIndex < 0 || !m_contextController) {
jkobus's avatar
jkobus committed
855 856 857 858 859 860 861
        stageChunkAction->setEnabled(false);
        unstageChunkAction->setEnabled(false);
    }
}

QString GitClient::makePatch(int diffFileIndex, int chunkIndex, bool revert) const
{
862
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
863 864 865 866 867
        return QString();

    if (diffFileIndex < 0 || chunkIndex < 0)
        return QString();

868
    QList<DiffEditor::FileData> fileDataList = m_contextController->diffFiles();
jkobus's avatar
jkobus committed
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891

    if (diffFileIndex >= fileDataList.count())
        return QString();

    const DiffEditor::FileData fileData = fileDataList.at(diffFileIndex);
    if (chunkIndex >= fileData.chunks.count())
        return QString();

    const DiffEditor::ChunkData chunkData = fileData.chunks.at(chunkIndex);
    const bool lastChunk = (chunkIndex == fileData.chunks.count() - 1);

    const QString fileName = revert
            ? fileData.rightFileInfo.fileName
            : fileData.leftFileInfo.fileName;

    return DiffEditor::DiffUtils::makePatch(chunkData,
                                QLatin1String("a/") + fileName,
                                QLatin1String("b/") + fileName,
                                lastChunk && fileData.lastChunkAtTheEndOfFile);
}

void GitClient::slotStageChunk()
{
892
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
893 894 895 896 897 898 899 900 901 902 903 904
        return;

    const QString patch = makePatch(m_contextDiffFileIndex,
                                    m_contextChunkIndex, false);
    if (patch.isEmpty())
        return;

    stage(patch, false);
}

void GitClient::slotUnstageChunk()
{
905
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
906 907 908 909 910 911 912 913 914