gitclient.cpp 137 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
hjk's avatar
hjk committed
4
** Contact: http://www.qt-project.org/legal
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8 9 10 11 12 13 14
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17 18 19 20 21 22 23 24 25
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
con's avatar
con committed
26 27
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
28
****************************************************************************/
hjk's avatar
hjk committed
29

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

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

42 43
#include <vcsbase/submitfilemodel.h>

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

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

62
#include <diffeditor/diffeditorconstants.h>
jkobus's avatar
jkobus committed
63 64 65
#include <diffeditor/diffeditorcontroller.h>
#include <diffeditor/diffeditordocument.h>
#include <diffeditor/diffeditormanager.h>
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 90
using namespace Core;

91 92 93
namespace Git {
namespace Internal {

94 95 96 97 98 99
// Suppress git diff warnings about "LF will be replaced by CRLF..." on Windows.
static inline unsigned diffExecutionFlags()
{
    return Utils::HostOsInfo::isWindowsHost() ? unsigned(VcsBase::VcsBasePlugin::SuppressStdErrInLogWindow) : 0u;
}

100 101
using VcsBase::VcsBasePlugin;

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);
jkobus's avatar
jkobus committed
127
    void slotDiffOutputReceived(const QString &contents);
128 129

private:
jkobus's avatar
jkobus committed
130 131 132 133 134 135
    void postCollectShowDescription(const QString &id);
    void postCollectDiffOutput(const QStringList &arguments);
    void postCollectDiffOutput(const QList<QStringList> &argumentsList);
    void addJob(VcsBase::Command *command, const QStringList &arguments);
    int timeout() const;
    QProcessEnvironment processEnvironment() const;
136
    Utils::FileName gitPath() const;
137

138
    QPointer<DiffEditor::DiffEditorController> m_controller;
139
    const QString m_workingDirectory;
jkobus's avatar
jkobus committed
140
    GitClient *m_gitClient;
141 142
    const QString m_waitMessage;

jkobus's avatar
jkobus committed
143
    QString m_id;
144 145
};

146
GitDiffHandler::GitDiffHandler(DiffEditor::DiffEditorController *controller,
jkobus's avatar
jkobus committed
147
               const QString &workingDirectory)
148
    : m_controller(controller),
149
      m_workingDirectory(workingDirectory),
jkobus's avatar
jkobus committed
150
      m_gitClient(GitPlugin::instance()->gitClient()),
151 152 153 154 155 156
      m_waitMessage(tr("Waiting for data..."))
{
}

void GitDiffHandler::diffFile(const QString &fileName)
{
jkobus's avatar
jkobus committed
157
    postCollectDiffOutput(QStringList() << QLatin1String("--") << fileName);
158 159
}

jkobus's avatar
jkobus committed
160 161
void GitDiffHandler::diffFiles(const QStringList &stagedFileNames,
                               const QStringList &unstagedFileNames)
162
{
jkobus's avatar
jkobus committed
163
    QList<QStringList> arguments;
164

jkobus's avatar
jkobus committed
165 166 167 168 169
    QStringList stagedArguments;
    stagedArguments << QLatin1String("--cached");
    stagedArguments << QLatin1String("--");
    stagedArguments << stagedFileNames;
    arguments << stagedArguments;
170

jkobus's avatar
jkobus committed
171 172 173 174 175 176
    if (!unstagedFileNames.isEmpty()) {
        QStringList unstagedArguments;
        unstagedArguments << QLatin1String("--");
        unstagedArguments << unstagedFileNames;
        arguments << unstagedArguments;
    }
177

jkobus's avatar
jkobus committed
178
    postCollectDiffOutput(arguments);
179 180 181 182
}

void GitDiffHandler::diffProjects(const QStringList &projectPaths)
{
jkobus's avatar
jkobus committed
183
    postCollectDiffOutput(QStringList() << QLatin1String("--") << projectPaths);
184 185 186 187
}

void GitDiffHandler::diffRepository()
{
jkobus's avatar
jkobus committed
188
    postCollectDiffOutput(QStringList());
189 190
}

191 192
void GitDiffHandler::diffBranch(const QString &branchName)
{
jkobus's avatar
jkobus committed
193
    postCollectDiffOutput(QStringList() << branchName);
194 195 196 197
}

void GitDiffHandler::show(const QString &id)
{
jkobus's avatar
jkobus committed
198 199
    m_id = id;
    postCollectShowDescription(id);
200 201
}

jkobus's avatar
jkobus committed
202
void GitDiffHandler::postCollectShowDescription(const QString &id)
203
{
204
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
205
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
206
        return;
jkobus's avatar
jkobus committed
207 208
    }

209
    m_controller->clear(m_waitMessage);
jkobus's avatar
jkobus committed
210 211 212 213 214 215 216
    VcsBase::Command *command = new VcsBase::Command(gitPath(),
                                                     m_workingDirectory,
                                                     processEnvironment());
    command->setCodec(m_gitClient->encoding(m_workingDirectory,
                                            "i18n.commitEncoding"));
    connect(command, SIGNAL(output(QString)),
            this, SLOT(slotShowDescriptionReceived(QString)));
217
    QStringList arguments;
jkobus's avatar
jkobus committed
218 219 220 221 222 223
    arguments << QLatin1String("show")
              << QLatin1String("-s")
              << QLatin1String(noColorOption)
              << QLatin1String(decorateOption)
              << id;
    command->addJob(arguments, timeout());
224 225 226
    command->execute();
}

Orgad Shaneh's avatar
Orgad Shaneh committed
227
void GitDiffHandler::slotShowDescriptionReceived(const QString &description)
228
{
229
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
230
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
231
        return;
jkobus's avatar
jkobus committed
232
    }
Jarek Kobus's avatar
Jarek Kobus committed
233

jkobus's avatar
jkobus committed
234
    postCollectDiffOutput(QStringList() << m_id + QLatin1Char('^') << m_id);
235

jkobus's avatar
jkobus committed
236
    // need to be called after postCollectDiffOutput(), since it clears the description
237
    m_controller->setDescription(
jkobus's avatar
jkobus committed
238 239
                m_gitClient->extendedShowDescription(m_workingDirectory,
                                                     description));
240 241
}

jkobus's avatar
jkobus committed
242 243 244 245
void GitDiffHandler::addJob(VcsBase::Command *command, const QStringList &arguments)
{
    QStringList args;
    args << QLatin1String("diff");
246
    if (m_controller->isIgnoreWhitespace())
jkobus's avatar
jkobus committed
247 248
        args << QLatin1String("--ignore-space-change");
    args << QLatin1String("--unified=") + QString::number(
249
                m_controller->contextLinesNumber());
jkobus's avatar
jkobus committed
250 251 252 253 254 255 256 257 258 259
    args << arguments;
    command->addJob(args, timeout());
}

void GitDiffHandler::postCollectDiffOutput(const QStringList &arguments)
{
    postCollectDiffOutput(QList<QStringList>() << arguments);
}

void GitDiffHandler::postCollectDiffOutput(const QList<QStringList> &argumentsList)
260
{
261
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
262
        deleteLater();
Orgad Shaneh's avatar
Orgad Shaneh committed
263
        return;
jkobus's avatar
jkobus committed
264 265
    }

266
    m_controller->clear(m_waitMessage);
jkobus's avatar
jkobus committed
267 268 269
    VcsBase::Command *command = new VcsBase::Command(gitPath(),
                                                     m_workingDirectory,
                                                     processEnvironment());
Orgad Shaneh's avatar
Orgad Shaneh committed
270
    command->setCodec(EditorManager::defaultTextCodec());
jkobus's avatar
jkobus committed
271 272
    connect(command, SIGNAL(output(QString)),
            this, SLOT(slotDiffOutputReceived(QString)));
273
    command->addFlags(diffExecutionFlags());
jkobus's avatar
jkobus committed
274 275 276 277

    for (int i = 0; i < argumentsList.count(); i++)
        addJob(command, argumentsList.at(i));

278 279 280
    command->execute();
}

jkobus's avatar
jkobus committed
281
void GitDiffHandler::slotDiffOutputReceived(const QString &contents)
282
{
283
    if (m_controller.isNull()) {
jkobus's avatar
jkobus committed
284
        deleteLater();
285
        return;
jkobus's avatar
jkobus committed
286
    }
287

jkobus's avatar
jkobus committed
288 289 290
    bool ok;
    QList<DiffEditor::FileData> fileDataList
            = DiffEditor::DiffUtils::readPatch(
291 292
                contents, m_controller->isIgnoreWhitespace(), &ok);
    m_controller->setDiffFiles(fileDataList, m_workingDirectory);
jkobus's avatar
jkobus committed
293
    deleteLater();
294 295
}

jkobus's avatar
jkobus committed
296
int GitDiffHandler::timeout() const
297
{
jkobus's avatar
jkobus committed
298
    return m_gitClient->settings()->intValue(GitSettings::timeoutKey);
299
}
300

jkobus's avatar
jkobus committed
301
QProcessEnvironment GitDiffHandler::processEnvironment() const
302
{
jkobus's avatar
jkobus committed
303 304
    return m_gitClient->processEnvironment();
}
305

306
Utils::FileName GitDiffHandler::gitPath() const
jkobus's avatar
jkobus committed
307
{
308
    return m_gitClient->gitExecutable();
jkobus's avatar
jkobus committed
309
}
310

jkobus's avatar
jkobus committed
311
/////////////////////////////////////
312

jkobus's avatar
jkobus committed
313 314 315 316 317 318 319 320 321 322 323 324
class GitDiffEditorReloader : public DiffEditor::DiffEditorReloader
{
    Q_OBJECT
public:
    enum DiffType {
        DiffRepository,
        DiffFile,
        DiffFileList,
        DiffProjectList,
        DiffBranch,
        DiffShow
    };
325

jkobus's avatar
jkobus committed
326 327 328
    GitDiffEditorReloader(QObject *parent);
    void setWorkingDirectory(const QString &workingDir) {
        m_workingDirectory = workingDir;
329
    }
jkobus's avatar
jkobus committed
330 331 332 333 334 335
    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
336
    }
jkobus's avatar
jkobus committed
337 338 339 340 341 342 343 344 345
    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;
346
    }
347 348


jkobus's avatar
jkobus committed
349 350
protected:
    void reload();
jkobus's avatar
jkobus committed
351

jkobus's avatar
jkobus committed
352 353
private:
    GitClient *m_gitClient;
354

jkobus's avatar
jkobus committed
355 356 357 358 359 360 361 362 363 364
    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;
};
365

jkobus's avatar
jkobus committed
366 367 368 369
GitDiffEditorReloader::GitDiffEditorReloader(QObject *parent)
    : DiffEditorReloader(parent),
      m_gitClient(GitPlugin::instance()->gitClient())
{
370 371
}

jkobus's avatar
jkobus committed
372
void GitDiffEditorReloader::reload()
373
{
374
    GitDiffHandler *handler = new GitDiffHandler(controller(),
375
                                                 m_workingDirectory);
jkobus's avatar
jkobus committed
376
    connect(handler, SIGNAL(destroyed()), this, SLOT(reloadFinished()));
377

jkobus's avatar
jkobus committed
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    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;
    }
400 401
}

jkobus's avatar
jkobus committed
402
///////////////////////////////
403

hjk's avatar
hjk committed
404
class BaseGitDiffArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
405
{
Friedemann Kleint's avatar
Friedemann Kleint committed
406
    Q_OBJECT
407

408
public:
409
    BaseGitDiffArgumentsWidget(GitClient *client, const QString &directory,
410
                               const QStringList &args) :
411
        m_workingDirectory(directory),
412
        m_client(client)
413
    {
414 415
        QTC_ASSERT(!directory.isEmpty(), return);
        QTC_ASSERT(m_client, return);
416

jkobus's avatar
jkobus committed
417 418 419 420 421 422 423 424 425 426 427
        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));
428 429

        setBaseArguments(args);
430 431
    }

432
protected:
433 434
    QString m_workingDirectory;
    GitClient *m_client;
435 436
    QToolButton *m_patienceButton;
    QToolButton *m_ignoreWSButton;
437 438
};

hjk's avatar
hjk committed
439
class GitBlameArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
440
{
Friedemann Kleint's avatar
Friedemann Kleint committed
441
    Q_OBJECT
442

443
public:
444 445 446 447
    GitBlameArgumentsWidget(Git::Internal::GitClient *client,
                            const QString &directory,
                            const QStringList &args,
                            const QString &revision, const QString &fileName) :
448
        m_editor(0),
449 450
        m_client(client),
        m_workingDirectory(directory),
451 452 453
        m_revision(revision),
        m_fileName(fileName)
    {
454 455 456
        mapSetting(addToggleButton(QString(), tr("Omit Date"),
                                   tr("Hide the date of a change from the output.")),
                   m_client->settings()->boolPointer(GitSettings::omitAnnotationDateKey));
457
        mapSetting(addToggleButton(QLatin1String("-w"), tr("Ignore Whitespace"),
458 459
                                   tr("Ignore whitespace only changes.")),
                   m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
460 461

        setBaseArguments(args);
462 463
    }

hjk's avatar
hjk committed
464
    void setEditor(VcsBase::VcsBaseEditorWidget *editor)
465
    {
466
        QTC_ASSERT(editor, return);
467 468 469
        m_editor = editor;
    }

470
    void executeCommand()
471
    {
472 473 474
        int line = -1;
        if (m_editor)
            line = m_editor->lineNumberOfCurrentEditor();
475
        m_client->blame(m_workingDirectory, baseArguments(), m_fileName, m_revision, line);
476 477 478
    }

private:
hjk's avatar
hjk committed
479
    VcsBase::VcsBaseEditorWidget *m_editor;
480 481
    GitClient *m_client;
    QString m_workingDirectory;
482 483 484 485
    QString m_revision;
    QString m_fileName;
};

486 487 488 489 490 491 492 493 494
class GitLogArgumentsWidget : public BaseGitDiffArgumentsWidget
{
    Q_OBJECT

public:
    GitLogArgumentsWidget(Git::Internal::GitClient *client,
                          const QString &directory,
                          bool enableAnnotationContextMenu,
                          const QStringList &args,
495
                          const QString &fileName) :
496 497 498
        BaseGitDiffArgumentsWidget(client, directory, args),
        m_client(client),
        m_workingDirectory(directory),
499
        m_enableAnnotationContextMenu(enableAnnotationContextMenu)
500
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
501
        QTC_ASSERT(!directory.isEmpty(), return);
502
        QToolButton *diffButton = addToggleButton(QLatin1String("--patch"), tr("Show Diff"),
503
                                              tr("Show difference."));
504 505 506 507 508 509 510 511 512 513 514
        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));
515
        setFileName(fileName);
516 517
    }

518
    void setFileName(const QString &fileNames)
519
    {
520
        m_fileName = fileNames;
521 522 523 524
    }

    void executeCommand()
    {
525
        m_client->log(m_workingDirectory, m_fileName, m_enableAnnotationContextMenu, baseArguments());
526 527 528 529 530 531
    }

private:
    GitClient *m_client;
    QString m_workingDirectory;
    bool m_enableAnnotationContextMenu;
532
    QString m_fileName;
533 534
};

Orgad Shaneh's avatar
Orgad Shaneh committed
535 536 537 538
class ConflictHandler : public QObject
{
    Q_OBJECT
public:
Orgad Shaneh's avatar
Orgad Shaneh committed
539
    ConflictHandler(VcsBase::Command *parentCommand,
Orgad Shaneh's avatar
Orgad Shaneh committed
540
                    const QString &workingDirectory,
541
                    const QString &command = QString())
Orgad Shaneh's avatar
Orgad Shaneh committed
542
        : QObject(parentCommand),
Orgad Shaneh's avatar
Orgad Shaneh committed
543 544 545
          m_workingDirectory(workingDirectory),
          m_command(command)
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
546
        if (parentCommand) {
Orgad Shaneh's avatar
Orgad Shaneh committed
547
            parentCommand->addFlags(VcsBasePlugin::ExpectRepoChanges);
548
            connect(parentCommand, SIGNAL(output(QString)), this, SLOT(readStdOut(QString)));
Orgad Shaneh's avatar
Orgad Shaneh committed
549 550
            connect(parentCommand, SIGNAL(errorText(QString)), this, SLOT(readStdErr(QString)));
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
551 552 553 554
    }

    ~ConflictHandler()
    {
555 556 557 558 559 560 561 562 563 564
        // 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
565
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
566 567
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
568 569
public slots:
    void readStdOut(const QString &data)
Orgad Shaneh's avatar
Orgad Shaneh committed
570 571
    {
        static QRegExp patchFailedRE(QLatin1String("Patch failed at ([^\\n]*)"));
572
        static QRegExp conflictedFilesRE(QLatin1String("Merge conflict in ([^\\n]*)"));
Orgad Shaneh's avatar
Orgad Shaneh committed
573 574
        if (patchFailedRE.indexIn(data) != -1)
            m_commit = patchFailedRE.cap(1);
575 576 577 578
        int fileIndex = -1;
        while ((fileIndex = conflictedFilesRE.indexIn(data, fileIndex + 1)) != -1) {
            m_files.append(conflictedFilesRE.cap(1));
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
579 580 581 582 583 584 585 586 587 588 589 590
    }

    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;
591
    QStringList m_files;
Orgad Shaneh's avatar
Orgad Shaneh committed
592 593
};

594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
class ProgressParser : public VcsBase::ProgressParser
{
public:
    ProgressParser() :
        m_progressExp(QLatin1String("\\((\\d+)/(\\d+)\\)")) // e.g. Rebasing (7/42)
    {
    }

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

private:
    QRegExp m_progressExp;
};


Orgad Shaneh's avatar
Orgad Shaneh committed
614

Orgad Shaneh's avatar
Orgad Shaneh committed
615
IEditor *locateEditor(const char *property, const QString &entry)
con's avatar
con committed
616
{
Orgad Shaneh's avatar
Orgad Shaneh committed
617
    foreach (IDocument *document, DocumentModel::openedDocuments())
618
        if (document->property(property).toString() == entry)
Orgad Shaneh's avatar
Orgad Shaneh committed
619
            return DocumentModel::editorsForDocument(document).first();
con's avatar
con committed
620 621 622
    return 0;
}

623 624 625
// Return converted command output, remove '\r' read on Windows
static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
{
626
    return Utils::SynchronousProcess::normalizeNewlines(QString::fromLocal8Bit(a));
627 628 629 630 631 632 633 634 635 636 637 638 639 640
}

// 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
641
static inline VcsBase::VcsBaseOutputWindow *outputWindow()
642
{
hjk's avatar
hjk committed
643
    return VcsBase::VcsBaseOutputWindow::instance();
644 645
}

646 647
static inline QString msgRepositoryNotFound(const QString &dir)
{
Tobias Hunger's avatar
Tobias Hunger committed
648
    return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
649 650 651 652
}

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

Friedemann Kleint's avatar
Friedemann Kleint committed
656 657 658 659 660
static inline QString msgCannotLaunch(const QString &binary)
{
    return GitClient::tr("Cannot launch \"%1\".").arg(QDir::toNativeSeparators(binary));
}

661 662
static inline QString currentDocumentPath()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
663
    if (IDocument *document= EditorManager::currentDocument())
664
        return QFileInfo(document->filePath()).path();
665
    return QString();
666 667
}

Orgad Shaneh's avatar
Orgad Shaneh committed
668 669 670 671 672 673
static inline QStringList statusArguments()
{
    return QStringList() << QLatin1String("-c") << QLatin1String("color.status=false")
                         << QLatin1String("status");
}

674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692
static inline void msgCannotRun(const QString &message, QString *errorMessage)
{
    if (errorMessage)
        *errorMessage = message;
    else
        outputWindow()->appendError(message);
}

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

    msgCannotRun(message, errorMessage);
}

693
// ---------------- GitClient
694 695 696

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

697 698
GitClient::GitClient(GitSettings *settings) :
    m_cachedGitVersion(0),
Tobias Hunger's avatar
Tobias Hunger committed
699
    m_msgWait(tr("Waiting for data...")),
700
    m_settings(settings),
jkobus's avatar
jkobus committed
701 702 703
    m_disableEditor(false),
    m_contextDiffFileIndex(-1),
    m_contextChunkIndex(-1)
con's avatar
con committed
704
{
705
    QTC_CHECK(settings);
Orgad Shaneh's avatar
Orgad Shaneh committed
706
    connect(ICore::instance(), SIGNAL(saveSettingsRequested()), this, SLOT(saveSettings()));
707 708 709
    m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
            .arg(QCoreApplication::applicationFilePath())
            .arg(QCoreApplication::applicationPid());
con's avatar
con committed
710 711 712 713 714 715 716 717
}

GitClient::~GitClient()
{
}

QString GitClient::findRepositoryForDirectory(const QString &dir)
{
718 719
    if (dir.isEmpty() || dir.endsWith(QLatin1String("/.git"))
            || dir.contains(QLatin1String("/.git/"))) {
720
        return QString();
721
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
722
    QDir directory(dir);
723 724 725
    QString dotGit = QLatin1String(GIT_DIRECTORY);
    // QFileInfo is outside loop, because it is faster this way
    QFileInfo fileInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
726
    do {
727 728 729 730 731 732 733
        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
734
    } while (!directory.isRoot() && directory.cdUp());
735
    return QString();
con's avatar
con committed
736 737
}

Orgad Shaneh's avatar
Orgad Shaneh committed
738
QString GitClient::findGitDirForRepository(const QString &repositoryDir) const
Orgad Shaneh's avatar
Orgad Shaneh committed
739
{
740 741 742 743
    static QHash<QString, QString> repoDirCache;
    QString &res = repoDirCache[repositoryDir];
    if (!res.isEmpty())
        return res;
Petar Perisin's avatar
Petar Perisin committed
744 745 746

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

747 748 749
    if (!QDir(res).isAbsolute())
        res.prepend(repositoryDir + QLatin1Char('/'));
    return res;
Orgad Shaneh's avatar
Orgad Shaneh committed
750 751
}

752 753 754 755 756 757 758 759 760
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
761
VcsBase::VcsBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
Tobias Hunger's avatar
Tobias Hunger committed
762
                                                               const QString &dynamicPropertyValue) const
763
{
hjk's avatar
hjk committed
764
    VcsBase::VcsBaseEditorWidget *rc = 0;
Orgad Shaneh's avatar
Orgad Shaneh committed
765
    IEditor *outputEditor = locateEditor(registerDynamicProperty, dynamicPropertyValue);
766 767 768
    if (!outputEditor)
        return 0;

769
    // Exists already
Orgad Shaneh's avatar
Orgad Shaneh committed
770
    EditorManager::activateEditor(outputEditor);
771
    outputEditor->document()->setContents(m_msgWait.toUtf8());
hjk's avatar
hjk committed
772
    rc = VcsBase::VcsBaseEditorWidget::getVcsBaseEditor(outputEditor);
773 774 775 776

    return rc;
}

777 778 779
DiffEditor::DiffEditorDocument *GitClient::createDiffEditor(const QString &documentId,
                                                            const QString &source,
                                                            const QString &title) const
780
{
jkobus's avatar
jkobus committed
781 782 783
    DiffEditor::DiffEditorDocument *diffEditorDocument = DiffEditor::DiffEditorManager::findOrCreate(documentId, title);
    QTC_ASSERT(diffEditorDocument, return 0);
    VcsBasePlugin::setSource(diffEditorDocument, source);
jkobus's avatar
jkobus committed
784 785 786 787

    connect(diffEditorDocument->controller(), SIGNAL(chunkActionsRequested(QMenu*,int,int)),
            this, SLOT(slotChunkActionsRequested(QMenu*,int,int)));

jkobus's avatar
jkobus committed
788
    return diffEditorDocument;
789
}
hjk's avatar
hjk committed
790

jkobus's avatar
jkobus committed
791 792 793 794 795 796 797 798 799 800
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;
801
    m_contextController = qobject_cast<DiffEditor::DiffEditorController *>(sender());
jkobus's avatar
jkobus committed
802

803
    if (m_contextDiffFileIndex < 0 || m_contextChunkIndex < 0 || !m_contextController) {
jkobus's avatar
jkobus committed
804 805 806 807 808 809 810
        stageChunkAction->setEnabled(false);
        unstageChunkAction->setEnabled(false);
    }
}

QString GitClient::makePatch(int diffFileIndex, int chunkIndex, bool revert) const
{
811
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
812 813 814 815 816
        return QString();

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

817
    QList<DiffEditor::FileData> fileDataList = m_contextController->diffFiles();
jkobus's avatar
jkobus committed
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840

    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()
{
841
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
842 843 844 845 846 847 848 849 850 851 852 853
        return;

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

    stage(patch, false);
}

void GitClient::slotUnstageChunk()
{
854
    if (m_contextController.isNull())
jkobus's avatar
jkobus committed
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872
        return;

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

    stage(patch, true);
}

void GitClient::stage(const QString &patch, bool revert)
{
    VcsBase::VcsBaseOutputWindow *outwin =
            VcsBase::VcsBaseOutputWindow::instance();
    QTemporaryFile patchFile;
    if (!patchFile.open())
        return;

873
    const QString baseDir = m_contextController->workingDirectory();
Orgad Shaneh's avatar
Orgad Shaneh committed
874
    QTextCodec *codec = EditorManager::defaultTextCodec();
jkobus's avatar
jkobus committed
875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
    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)
                outwin->append(tr("Chunk successfully unstaged"));
            else
                outwin->append(tr("Chunk successfully staged"));
        } else {
            outwin->append(errorMessage);
        }
894
        m_contextController->requestReload();
jkobus's avatar
jkobus committed
895 896 897 898 899
    } else {
        outwin->appendError(errorMessage);
    }
}

con's avatar
con committed
900 901 902 903
/* 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). */
Orgad Shaneh's avatar
Orgad Shaneh committed
904
VcsBase::VcsBaseEditorWidget *GitClient::createVcsEditor(
Orgad Shaneh's avatar
Orgad Shaneh committed
905
        Id id,
Orgad Shaneh's avatar
Orgad Shaneh committed
906 907 908 909 910 911
        QString title,
        const QString &source, // Source file or directory
        CodecType codecType,
        const char *registerDynamicProperty, // Dynamic property and value to identify that editor
        const QString &dynamicPropertyValue,
        VcsBase::VcsBaseEditorParameterWidget *configWidget) const
con's avatar
con committed
912
{
hjk's avatar
hjk committed
913
    VcsBase::VcsBaseEditorWidget *rc = 0;
914
    QTC_CHECK(!findExistingVCSEditor(registerDynamicProperty, dynamicPropertyValue));