gitclient.cpp 151 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/diffeditor.h>
63 64
#include <diffeditor/diffeditorconstants.h>

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

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

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

84 85 86
namespace Git {
namespace Internal {

87 88 89 90 91 92
// 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;
}

93 94
using VcsBase::VcsBasePlugin;

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

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

109
    GitDiffSwitcher(Core::IEditor *parentEditor, GitClient *gitClient)
110
        : QObject(parentEditor),
111
          m_editor(parentEditor),
112
          m_gitClient(gitClient)
113
    {
114 115 116 117
        m_usingDiffEditor = gitClient->settings()->boolValue(GitSettings::useDiffEditorKey);
        QIcon actionIcon = m_usingDiffEditor
                ? QIcon(QLatin1String(Core::Constants::ICON_TEXT_DIFF))
                : QIcon(QLatin1String(Core::Constants::ICON_SIDE_BY_SIDE_DIFF));
118

119 120 121
        const QString actionToolTip = m_usingDiffEditor
                ? tr("Switch to Text Diff Editor")
                : tr("Switch to Side By Side Diff Editor");
122 123 124

        QAction *switchAction = new QAction(actionIcon, actionToolTip, parentEditor);
        parentEditor->toolBar()->addAction(switchAction);
125 126 127

        // must be queued connection because execute() removes the editor & tool bar that the action was added to
        connect(switchAction, SIGNAL(triggered()), this, SLOT(execute()), Qt::QueuedConnection);
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    }

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

public slots:
    void execute();

private:
148
    Core::IEditor *m_editor;
149 150 151
    GitClient *m_gitClient;
    QString m_workingDirectory;
    DiffType m_diffType;
152
    bool m_usingDiffEditor;
153 154 155 156 157 158 159 160 161 162 163 164
    QString m_fileName;
    QStringList m_stagedFiles;
    QStringList m_unstagedFiles;
    QStringList m_projectFiles;
    QString m_branchName;
    QString m_id;
    QString m_displayName;
    QStringList m_baseArguments;
};

void GitDiffSwitcher::execute()
{
165
    m_gitClient->settings()->setValue(GitSettings::useDiffEditorKey, !m_usingDiffEditor);
166 167
    switch (m_diffType) {
    case DiffRepository:
168
        m_gitClient->diff(m_workingDirectory, QStringList(), QStringList());
169 170
        break;
    case DiffFile:
171
        m_gitClient->diff(m_workingDirectory, m_fileName);
172 173
        break;
    case DiffFileList:
174
        m_gitClient->diff(m_workingDirectory, m_unstagedFiles, m_stagedFiles);
175 176
        break;
    case DiffProjectList:
177
        m_gitClient->diff(m_workingDirectory, m_projectFiles, QStringList());
178 179
        break;
    case DiffBranch:
180
        m_gitClient->diffBranch(m_workingDirectory, m_baseArguments, m_branchName);
181 182
        break;
    case DiffShow:
183
        m_gitClient->show(m_fileName, m_id, m_baseArguments, m_displayName);
184 185 186 187
        break;
    default:
        break;
    }
188
    Core::EditorManager::closeEditor(m_editor, false);
189 190
}

191 192 193 194 195
class GitDiffHandler : public QObject
{
    Q_OBJECT

public:
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
    enum RevisionType {
        WorkingTree,
        Index,
        Other
    };

    struct Revision {
        Revision() : type(WorkingTree) { }
        Revision(RevisionType t) : type(t) { }
        Revision(RevisionType t, const QString &i) : type(t), id(i) { }
        RevisionType type;
        QString id; // can be sha or HEAD
        QString infoText() const
        {
            switch (type) {
            case WorkingTree: return tr("Working tree");
            case Index:       return tr("Index");
            default:          return id;
            }
        }
    };

218
    GitDiffHandler(DiffEditor::DiffEditor *editor,
219
                   const QString &gitPath,
220 221 222 223 224 225
                   const QString &workingDirectory,
                   const QProcessEnvironment &environment,
                   int timeout);

    // index -> working tree
    void diffFile(const QString &fileName);
226 227
    // stagedFileNames:   HEAD -> index
    // unstagedFileNames: index -> working tree
228 229 230 231 232
    void diffFiles(const QStringList &stagedFileNames, const QStringList &unstagedFileNames);
    // index -> working tree
    void diffProjects(const QStringList &projectPaths);
    // index -> working tree
    void diffRepository();
233 234 235 236
    // branch HEAD -> working tree
    void diffBranch(const QString &branchName);
    // id^ -> id
    void show(const QString &id);
237 238

private slots:
239 240 241
    void slotShowDescriptionReceived(const QString &data);
    void slotFileListReceived(const QString &fileList);
    void slotFileContentsReceived(const QString &contents);
242 243

private:
244
    void collectShowDescription(const QString &id);
245
    void collectFilesList(const QStringList &additionalArguments);
246
    void prepareForCollection();
247 248 249 250
    void collectFilesContents();
    void feedEditor();
    QString workingTreeContents(const QString &fileName) const;

251
    QPointer<DiffEditor::DiffEditor> m_editor;
252 253 254 255 256 257
    const QString m_gitPath;
    const QString m_workingDirectory;
    const QProcessEnvironment m_processEnvironment;
    const int m_timeout;
    const QString m_waitMessage;

258 259 260 261 262
    struct RevisionRange {
        RevisionRange() { }
        RevisionRange(const Revision &b, const Revision &e) : begin(b), end(e) { }
        Revision begin;
        Revision end;
263 264
    };

265 266 267 268 269 270
    // filename, revision range
    QMap<QString, QList<RevisionRange> > m_requestedRevisionRanges;
    // filename, revision, dummy
    QMap<QString, QMap<Revision, bool> > m_pendingRevisions;
    // filename, revision, contents
    QMap<QString, QMap<Revision, QString> > m_collectedRevisions;
271

272
    RevisionRange m_requestedRevisionRange;
273 274
};

275 276 277 278 279 280 281
inline bool operator<(const GitDiffHandler::Revision &rev1, const GitDiffHandler::Revision &rev2)
{
    if (rev1.type != rev2.type)
        return rev1.type < rev2.type;
    return rev1.id < rev2.id;
}

282
GitDiffHandler::GitDiffHandler(DiffEditor::DiffEditor *editor,
283
               const QString &gitPath,
284 285 286
               const QString &workingDirectory,
               const QProcessEnvironment &environment,
               int timeout)
287 288
    : m_editor(editor),
      m_gitPath(gitPath),
289 290 291 292 293 294 295 296 297
      m_workingDirectory(workingDirectory),
      m_processEnvironment(environment),
      m_timeout(timeout),
      m_waitMessage(tr("Waiting for data..."))
{
}

void GitDiffHandler::diffFile(const QString &fileName)
{
298
    m_requestedRevisionRange = RevisionRange(
299
                Revision(Index),
300 301 302
                Revision(WorkingTree));

    collectFilesList(QStringList() << QLatin1String("--") << fileName);
303 304 305 306
}

void GitDiffHandler::diffFiles(const QStringList &stagedFileNames, const QStringList &unstagedFileNames)
{
307 308
    RevisionRange stagedRange = RevisionRange(
                Revision(Other, QLatin1String(HEAD)),
309
                Revision(Index));
310 311 312
    RevisionRange unstagedRange = RevisionRange(
                Revision(Index),
                Revision(WorkingTree));
313

314 315 316 317 318 319 320
    for (int i = 0; i < stagedFileNames.count(); i++)
        m_requestedRevisionRanges[stagedFileNames.at(i)].append(stagedRange);

    for (int i = 0; i < unstagedFileNames.count(); i++)
        m_requestedRevisionRanges[unstagedFileNames.at(i)].append(unstagedRange);

    prepareForCollection();
321 322 323 324 325
    collectFilesContents();
}

void GitDiffHandler::diffProjects(const QStringList &projectPaths)
{
326
    m_requestedRevisionRange = RevisionRange(
327
                Revision(Index),
328 329
                Revision(WorkingTree));

330 331 332 333 334
    collectFilesList(QStringList() << QLatin1String("--") << projectPaths);
}

void GitDiffHandler::diffRepository()
{
335
    m_requestedRevisionRange = RevisionRange(
336
                Revision(Index),
337 338
                Revision(WorkingTree));

339 340 341
    collectFilesList(QStringList());
}

342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
void GitDiffHandler::diffBranch(const QString &branchName)
{
    m_requestedRevisionRange = RevisionRange(
                Revision(Other, branchName),
                Revision(WorkingTree));

    collectFilesList(QStringList() << branchName);
}

void GitDiffHandler::show(const QString &id)
{
    Revision begin(Other, id + QLatin1Char('^'));
    Revision end(Other, id);
    m_requestedRevisionRange = RevisionRange(begin, end);

357 358 359 360 361
    collectShowDescription(id);
}

void GitDiffHandler::collectShowDescription(const QString &id)
{
362 363
    if (m_editor.isNull())
        return;
364 365
    m_editor->clear(m_waitMessage);
    VcsBase::Command *command = new VcsBase::Command(m_gitPath, m_workingDirectory, m_processEnvironment);
366
    command->setCodec(m_editor->codec());
367
    connect(command, SIGNAL(output(QString)), this, SLOT(slotShowDescriptionReceived(QString)));
368
    QStringList arguments;
369
    arguments << QLatin1String("show") << QLatin1String("-s")
370
              << QLatin1String(noColorOption) << QLatin1String(decorateOption) << id;
371 372 373 374
    command->addJob(arguments, m_timeout);
    command->execute();
}

375
void GitDiffHandler::slotShowDescriptionReceived(const QString &description)
376
{
377 378
    if (m_editor.isNull())
        return;
Jarek Kobus's avatar
Jarek Kobus committed
379 380 381

    m_editor->setDescription(GitPlugin::instance()->gitClient()->
                             extendedShowDescription(m_workingDirectory, description));
382 383 384 385

    collectFilesList(QStringList()
                     << m_requestedRevisionRange.begin.id
                     << m_requestedRevisionRange.end.id);
386 387
}

388 389
void GitDiffHandler::collectFilesList(const QStringList &additionalArguments)
{
390 391
    if (m_editor.isNull())
        return;
392
    m_editor->clear(m_waitMessage);
393
    VcsBase::Command *command = new VcsBase::Command(m_gitPath, m_workingDirectory, m_processEnvironment);
394
    command->setCodec(m_editor->codec());
395
    connect(command, SIGNAL(output(QString)), this, SLOT(slotFileListReceived(QString)));
396 397 398
    QStringList arguments;
    arguments << QLatin1String("diff") << QLatin1String("--name-only") << additionalArguments;
    command->addJob(arguments, m_timeout);
399
    command->addFlags(diffExecutionFlags());
400 401 402
    command->execute();
}

403
void GitDiffHandler::slotFileListReceived(const QString &fileList)
404 405 406 407
{
    if (m_editor.isNull())
        return;

408 409 410 411 412
    QStringList fileNames = fileList.split(QLatin1Char('\n'), QString::SkipEmptyParts);
    fileNames.removeDuplicates();

    for (int i = 0; i < fileNames.count(); i++)
        m_requestedRevisionRanges[fileNames.at(i)].append(m_requestedRevisionRange);
413

414
    prepareForCollection();
415 416 417
    collectFilesContents();
}

418
void GitDiffHandler::prepareForCollection()
419
{
420 421 422 423 424 425 426 427 428 429 430 431
    QMap<QString, QList<RevisionRange> >::const_iterator it
            = m_requestedRevisionRanges.constBegin();
    QMap<QString, QList<RevisionRange> >::const_iterator itEnd
            = m_requestedRevisionRanges.constEnd();
    while (it != itEnd) {
        const QString fileName = it.key();
        const QList<RevisionRange> &ranges = it.value();
        for (int i = 0; i < ranges.count(); i++) {
            const RevisionRange &range = ranges.at(i);
            m_pendingRevisions[fileName][range.begin] = false;
            m_pendingRevisions[fileName][range.end] = false;
        }
432

433 434 435
        ++it;
    }
}
436

437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
void GitDiffHandler::collectFilesContents()
{
    QMap<QString, QMap<Revision, bool> >::iterator itFile
            = m_pendingRevisions.begin();
    QMap<QString, QMap<Revision, bool> >::iterator itFileEnd
            = m_pendingRevisions.end();
    while (itFile != itFileEnd) {
        const QString fileName = itFile.key();
        QMap<Revision, bool> &revisions = itFile.value();
        QMap<Revision, bool>::iterator itRev
                = revisions.begin();
        QMap<Revision, bool>::iterator itRevEnd
                = revisions.end();
        while (itRev != itRevEnd) {
            const Revision revision = itRev.key();
            if (revision.type == WorkingTree) {
                // collect file here

                m_collectedRevisions[fileName][revision] = workingTreeContents(fileName);

                itRev = revisions.erase(itRev); // iterate to the next revision
            } else {
                // prepare job here

                VcsBase::Command *command = new VcsBase::Command(m_gitPath, m_workingDirectory, m_processEnvironment);
462
                if (m_editor)
463
                    command->setCodec(m_editor->codec());
464
                connect(command, SIGNAL(output(QString)), this, SLOT(slotFileContentsReceived(QString)));
465 466 467

                QString revisionArgument = (revision.type == Other)
                        ? revision.id : QString();
468
                revisionArgument += QLatin1Char(':');
469 470 471 472 473 474 475 476
                QStringList arguments;
                arguments << QLatin1String("show") << revisionArgument + fileName;
                command->addJob(arguments, m_timeout);
                command->execute();

                return;
            }
        }
477

478
        itFile = m_pendingRevisions.erase(itFile); // iterate to the next file
479
    }
480 481

    feedEditor();
482 483
}

484
void GitDiffHandler::slotFileContentsReceived(const QString &contents)
485 486 487 488
{
    if (m_editor.isNull())
        return;

489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    QMap<QString, QMap<Revision, bool> >::iterator itFile
            = m_pendingRevisions.begin();
    QMap<QString, QMap<Revision, bool> >::iterator itFileEnd
            = m_pendingRevisions.end();
    if (itFile != itFileEnd) {
        const QString fileName = itFile.key();
        QMap<Revision, bool> &revisions = itFile.value();
        QMap<Revision, bool>::iterator itRev
                = revisions.begin();
        QMap<Revision, bool>::iterator itRevEnd
                = revisions.end();
        if (itRev != itRevEnd) {
            m_collectedRevisions[fileName][itRev.key()] = contents;

            itRev = revisions.erase(itRev);
            if (revisions.isEmpty())
                m_pendingRevisions.erase(itFile);
        }
    }
508 509 510 511 512 513

    collectFilesContents();
}

void GitDiffHandler::feedEditor()
{
jkobus's avatar
jkobus committed
514
    QList<DiffEditor::DiffEditorController::DiffFilesContents> list;
515

516 517 518 519 520 521 522 523 524 525 526
    QMap<QString, QList<RevisionRange> >::const_iterator itFile
            = m_requestedRevisionRanges.constBegin();
    QMap<QString, QList<RevisionRange> >::const_iterator itFileEnd
            = m_requestedRevisionRanges.constEnd();
    while (itFile != itFileEnd) {
        const QString fileName = itFile.key();
        const QList<RevisionRange> &ranges = itFile.value();
        for (int i = 0; i < ranges.count(); i++) {
            const Revision leftRevision = ranges.at(i).begin;
            const Revision rightRevision = ranges.at(i).end;

jkobus's avatar
jkobus committed
527 528
            DiffEditor::DiffEditorController::DiffFilesContents dfc;
            dfc.leftFileInfo = DiffEditor::DiffEditorController::DiffFileInfo(fileName, leftRevision.infoText());
529
            dfc.leftText = m_collectedRevisions[fileName][leftRevision];
jkobus's avatar
jkobus committed
530
            dfc.rightFileInfo = DiffEditor::DiffEditorController::DiffFileInfo(fileName, rightRevision.infoText());
531
            dfc.rightText = m_collectedRevisions[fileName][rightRevision];
532 533
            list.append(dfc);
        }
534 535

        ++itFile;
536
    }
537

538
    m_editor->setDiff(list, m_workingDirectory);
539 540 541 542 543 544 545 546 547
    deleteLater();
}

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

    QFile file(absoluteFileName);
Orgad Shaneh's avatar
Orgad Shaneh committed
548
    if (file.open(QIODevice::ReadOnly | QIODevice::Text))
549
        return m_editor->codec()->toUnicode(file.readAll());
550 551 552 553 554
    return QString();
}

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

hjk's avatar
hjk committed
555
class BaseGitDiffArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
556
{
Friedemann Kleint's avatar
Friedemann Kleint committed
557
    Q_OBJECT
558

559
public:
560
    BaseGitDiffArgumentsWidget(GitClient *client, const QString &directory,
561
                               const QStringList &args) :
562
        m_workingDirectory(directory),
563
        m_client(client)
564
    {
565 566
        QTC_ASSERT(!directory.isEmpty(), return);
        QTC_ASSERT(m_client, return);
567

568 569 570
        m_patienceButton = addToggleButton(QLatin1String("--patience"), tr("Patience"),
                                           tr("Use the patience algorithm for calculating the differences."));
        mapSetting(m_patienceButton, client->settings()->boolPointer(GitSettings::diffPatienceKey));
571
        m_ignoreWSButton = addToggleButton(QLatin1String("--ignore-space-change"), tr("Ignore Whitespace"),
572 573
                                           tr("Ignore whitespace only changes."));
        mapSetting(m_ignoreWSButton, m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInDiffKey));
574 575

        setBaseArguments(args);
576 577
    }

578
protected:
579 580
    QString m_workingDirectory;
    GitClient *m_client;
581 582
    QToolButton *m_patienceButton;
    QToolButton *m_ignoreWSButton;
583 584 585 586
};

class GitCommitDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
587
    Q_OBJECT
588

589
public:
590
    GitCommitDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
Orgad Shaneh's avatar
Orgad Shaneh committed
591
                                 const QStringList &unstaged, const QStringList &staged) :
592 593 594 595 596 597 598 599 600 601
        BaseGitDiffArgumentsWidget(client, directory, QStringList())
    {
        setFileNames(unstaged, staged);
    }

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

603
    void executeCommand()
604
    {
605
        m_client->diff(m_workingDirectory, m_unstagedFileNames, m_stagedFileNames);
606 607 608
    }

private:
609 610
    QStringList m_unstagedFileNames;
    QStringList m_stagedFileNames;
611 612 613 614
};

class GitFileDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
615
    Q_OBJECT
616
public:
617
    GitFileDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
Orgad Shaneh's avatar
Orgad Shaneh committed
618 619
                               const QString &file) :
        BaseGitDiffArgumentsWidget(client, directory, QStringList()),
620 621 622
        m_fileName(file)
    { }

623
    void executeCommand()
624
    {
625
        m_client->diff(m_workingDirectory, m_fileName);
626 627 628 629 630 631 632 633
    }

private:
    const QString m_fileName;
};

class GitBranchDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
{
Friedemann Kleint's avatar
Friedemann Kleint committed
634
    Q_OBJECT
635
public:
636
    GitBranchDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
637
                                 const QStringList &args, const QString &branch) :
638
        BaseGitDiffArgumentsWidget(client, directory, args),
639 640 641
        m_branchName(branch)
    { }

642
    void executeCommand()
643
    {
644
        m_client->diffBranch(m_workingDirectory, baseArguments(), m_branchName);
645 646 647 648 649 650
    }

private:
    const QString m_branchName;
};

651
class GitShowArgumentsWidget : public BaseGitDiffArgumentsWidget
652
{
Friedemann Kleint's avatar
Friedemann Kleint committed
653
    Q_OBJECT
654

655
public:
656
    GitShowArgumentsWidget(Git::Internal::GitClient *client,
657 658 659
                           const QString &directory,
                           const QStringList &args,
                           const QString &id) :
660
        BaseGitDiffArgumentsWidget(client, directory, args),
661 662
        m_client(client),
        m_workingDirectory(directory),
663 664
        m_id(id)
    {
665 666 667 668 669 670 671 672
        QList<ComboBoxItem> prettyChoices;
        prettyChoices << ComboBoxItem(tr("oneline"), QLatin1String("oneline"))
                      << ComboBoxItem(tr("short"), QLatin1String("short"))
                      << ComboBoxItem(tr("medium"), QLatin1String("medium"))
                      << ComboBoxItem(tr("full"), QLatin1String("full"))
                      << ComboBoxItem(tr("fuller"), QLatin1String("fuller"))
                      << ComboBoxItem(tr("email"), QLatin1String("email"))
                      << ComboBoxItem(tr("raw"), QLatin1String("raw"));
673
        mapSetting(addComboBox(QStringList(QLatin1String("--pretty=%1")), prettyChoices),
674
                   m_client->settings()->intPointer(GitSettings::showPrettyFormatKey));
675 676 677
    }

    void executeCommand()
678
    {
679
        m_client->show(m_workingDirectory, m_id, baseArguments());
680 681 682
    }

private:
683 684
    GitClient *m_client;
    QString m_workingDirectory;
685
    QString m_id;
686 687
};

hjk's avatar
hjk committed
688
class GitBlameArgumentsWidget : public VcsBase::VcsBaseEditorParameterWidget
689
{
Friedemann Kleint's avatar
Friedemann Kleint committed
690
    Q_OBJECT
691

692
public:
693 694 695 696
    GitBlameArgumentsWidget(Git::Internal::GitClient *client,
                            const QString &directory,
                            const QStringList &args,
                            const QString &revision, const QString &fileName) :
697
        m_editor(0),
698 699
        m_client(client),
        m_workingDirectory(directory),
700 701 702
        m_revision(revision),
        m_fileName(fileName)
    {
703 704 705
        mapSetting(addToggleButton(QString(), tr("Omit Date"),
                                   tr("Hide the date of a change from the output.")),
                   m_client->settings()->boolPointer(GitSettings::omitAnnotationDateKey));
706
        mapSetting(addToggleButton(QLatin1String("-w"), tr("Ignore Whitespace"),
707 708
                                   tr("Ignore whitespace only changes.")),
                   m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
709 710

        setBaseArguments(args);
711 712
    }

hjk's avatar
hjk committed
713
    void setEditor(VcsBase::VcsBaseEditorWidget *editor)
714
    {
715
        QTC_ASSERT(editor, return);
716 717 718
        m_editor = editor;
    }

719
    void executeCommand()
720
    {
721 722 723
        int line = -1;
        if (m_editor)
            line = m_editor->lineNumberOfCurrentEditor();
724
        m_client->blame(m_workingDirectory, baseArguments(), m_fileName, m_revision, line);
725 726 727
    }

private:
hjk's avatar
hjk committed
728
    VcsBase::VcsBaseEditorWidget *m_editor;
729 730
    GitClient *m_client;
    QString m_workingDirectory;
731 732 733 734
    QString m_revision;
    QString m_fileName;
};

735 736 737 738 739 740 741 742 743
class GitLogArgumentsWidget : public BaseGitDiffArgumentsWidget
{
    Q_OBJECT

public:
    GitLogArgumentsWidget(Git::Internal::GitClient *client,
                          const QString &directory,
                          bool enableAnnotationContextMenu,
                          const QStringList &args,
744
                          const QString &fileName) :
745 746 747
        BaseGitDiffArgumentsWidget(client, directory, args),
        m_client(client),
        m_workingDirectory(directory),
748
        m_enableAnnotationContextMenu(enableAnnotationContextMenu)
749
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
750
        QTC_ASSERT(!directory.isEmpty(), return);
751
        QToolButton *diffButton = addToggleButton(QLatin1String("--patch"), tr("Show Diff"),
752
                                              tr("Show difference."));
753 754 755 756 757 758 759 760 761 762 763
        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));
764
        setFileName(fileName);
765 766
    }

767
    void setFileName(const QString &fileNames)
768
    {
769
        m_fileName = fileNames;
770 771 772 773
    }

    void executeCommand()
    {
774
        m_client->log(m_workingDirectory, m_fileName, m_enableAnnotationContextMenu, baseArguments());
775 776 777 778 779 780
    }

private:
    GitClient *m_client;
    QString m_workingDirectory;
    bool m_enableAnnotationContextMenu;
781
    QString m_fileName;
782 783
};

784 785 786 787
class ConflictHandler : public QObject
{
    Q_OBJECT
public:
Orgad Shaneh's avatar
Orgad Shaneh committed
788
    ConflictHandler(VcsBase::Command *parentCommand,
789
                    const QString &workingDirectory,
790
                    const QString &command = QString())
Orgad Shaneh's avatar
Orgad Shaneh committed
791
        : QObject(parentCommand),
792 793 794
          m_workingDirectory(workingDirectory),
          m_command(command)
    {
Orgad Shaneh's avatar
Orgad Shaneh committed
795
        if (parentCommand) {
Orgad Shaneh's avatar
Orgad Shaneh committed
796
            parentCommand->addFlags(VcsBasePlugin::ExpectRepoChanges);
797
            connect(parentCommand, SIGNAL(output(QString)), this, SLOT(readStdOut(QString)));
Orgad Shaneh's avatar
Orgad Shaneh committed
798 799
            connect(parentCommand, SIGNAL(errorText(QString)), this, SLOT(readStdErr(QString)));
        }
800 801 802 803
    }

    ~ConflictHandler()
    {
804 805 806 807 808 809 810 811 812 813
        // 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
814
        }
815 816
    }

817 818
public slots:
    void readStdOut(const QString &data)
819 820
    {
        static QRegExp patchFailedRE(QLatin1String("Patch failed at ([^\\n]*)"));
821
        static QRegExp conflictedFilesRE(QLatin1String("Merge conflict in ([^\\n]*)"));
822 823
        if (patchFailedRE.indexIn(data) != -1)
            m_commit = patchFailedRE.cap(1);
824 825 826 827
        int fileIndex = -1;
        while ((fileIndex = conflictedFilesRE.indexIn(data, fileIndex + 1)) != -1) {
            m_files.append(conflictedFilesRE.cap(1));
        }
828 829 830 831 832 833 834 835 836 837 838 839
    }

    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;
840
    QStringList m_files;
841 842
};

843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862
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
863

hjk's avatar
hjk committed
864
Core::IEditor *locateEditor(const char *property, const QString &entry)
con's avatar
con committed
865
{
866 867 868
    foreach (Core::IDocument *document, Core::EditorManager::documentModel()->openedDocuments())
        if (document->property(property).toString() == entry)
            return Core::EditorManager::documentModel()->editorsForDocument(document).first();
con's avatar
con committed
869 870 871
    return 0;
}

872 873 874
// Return converted command output, remove '\r' read on Windows
static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
{
875
    return Utils::SynchronousProcess::normalizeNewlines(QString::fromLocal8Bit(a));
876 877 878 879 880 881 882 883 884 885 886 887 888 889
}

// 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
890
static inline VcsBase::VcsBaseOutputWindow *outputWindow()
891
{
hjk's avatar
hjk committed
892
    return VcsBase::VcsBaseOutputWindow::instance();
893 894
}

895 896
static inline QString msgRepositoryNotFound(const QString &dir)
{
Tobias Hunger's avatar
Tobias Hunger committed
897
    return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
898 899 900 901
}

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

Friedemann Kleint's avatar
Friedemann Kleint committed
905 906 907 908 909
static inline QString msgCannotLaunch(const QString &binary)
{
    return GitClient::tr("Cannot launch \"%1\".").arg(QDir::toNativeSeparators(binary));
}

910 911
static inline QString currentDocumentPath()
{
912 913
    if (Core::IDocument *document= Core::EditorManager::currentDocument())
        return QFileInfo(document->filePath()).path();
914
    return QString();
915 916
}

Orgad Shaneh's avatar
Orgad Shaneh committed
917 918 919 920 921 922
static inline QStringList statusArguments()
{
    return QStringList() << QLatin1String("-c") << QLatin1String("color.status=false")
                         << QLatin1String("status");
}

923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941
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);
}

942
// ---------------- GitClient
943 944 945

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

946 947
GitClient::GitClient(GitSettings *settings) :
    m_cachedGitVersion(0),
Tobias Hunger's avatar
Tobias Hunger committed
948
    m_msgWait(tr("Waiting for data...")),
949 950
    m_settings(settings),
    m_disableEditor(false)
con's avatar
con committed
951
{
952
    QTC_CHECK(settings);
hjk's avatar
hjk committed
953
    connect(Core::ICore::instance(), SIGNAL(saveSettingsRequested()), this, SLOT(saveSettings()));
954 955 956
    m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
            .arg(QCoreApplication::applicationFilePath())
            .arg(QCoreApplication::applicationPid());
con's avatar
con committed
957 958 959 960 961 962 963 964
}

GitClient::~GitClient()
{
}

QString GitClient::findRepositoryForDirectory(const QString &dir)
{
965 966
    if (dir.isEmpty() || dir.endsWith(QLatin1String("/.git"))
            || dir.contains(QLatin1String("/.git/"))) {
967
        return QString();
968
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
969
    QDir directory(dir);
970 971 972
    QString dotGit = QLatin1String(GIT_DIRECTORY);
    // QFileInfo is outside loop, because it is faster this way
    QFileInfo fileInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
973
    do {
974 975 976 977 978 979 980
        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
981
    } while (!directory.isRoot() && directory.cdUp());
982
    return QString();
con's avatar
con committed
983 984
}

Orgad Shaneh's avatar
Orgad Shaneh committed
985
QString GitClient::findGitDirForRepository(const QString &repositoryDir) const
986
{
987 988 989 990
    static QHash<QString, QString> repoDirCache;
    QString &res = repoDirCache[repositoryDir];
    if (!res.isEmpty())
        return res;
Petar Perisin's avatar
Petar Perisin committed
991 992 993

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

994 995 996
    if (!QDir(res).isAbsolute())
        res.prepend(repositoryDir + QLatin1Char('/'));
    return res;
997 998
}

999 1000 1001 1002 1003 1004 1005 1006 1007
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
1008
VcsBase::VcsBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
Tobias Hunger's avatar
Tobias Hunger committed
1009
                                                               const QString &dynamicPropertyValue) const
1010
{
hjk's avatar
hjk committed
1011
    VcsBase::VcsBaseEditorWidget *rc = 0;
hjk's avatar
hjk committed
1012
    Core::IEditor *outputEditor = locateEditor(registerDynamicProperty, dynamicPropertyValue);
1013 1014 1015
    if (!outputEditor)
        return 0;

1016
    // Exists already
Eike Ziller's avatar
Eike Ziller committed
1017
    Core::EditorManager::activateEditor(outputEditor);
1018
    outputEditor->document()->setContents(m_msgWait.toUtf8());
hjk's avatar
hjk committed
1019
    rc = VcsBase::VcsBaseEditorWidget::getVcsBaseEditor(outputEditor);
1020 1021 1022 1023

    return rc;
}

1024 1025
DiffEditor::DiffEditor *GitClient::findExistingDiffEditor(const char *registerDynamicProperty,
    const QString &dynamicPropertyValue) const
1026
{
1027 1028 1029 1030 1031
    DiffEditor::DiffEditor *diffEditor = qobject_cast<DiffEditor::DiffEditor *>(
                locateEditor(registerDynamicProperty, dynamicPropertyValue));
    if (diffEditor) {
        diffEditor->document()->setContents(m_msgWait.toUtf8());
        Core::EditorManager::activateEditor(diffEditor);
1032
    }
1033
    return diffEditor;
1034 1035
}

1036
DiffEditor::DiffEditor *GitClient::createDiffEditor(const char *registerDynamicProperty,
1037 1038 1039 1040
                                                    const QString &dynamicPropertyValue,
                                                    const QString &source,
                                                    const QString &titlePattern,
                                                    const Core::Id editorId) const
1041 1042 1043 1044 1045 1046
{
    QString title = titlePattern;
    DiffEditor::DiffEditor *diffEditor = qobject_cast<DiffEditor::DiffEditor *>(
                Core::EditorManager::openEditorWithContents(editorId, &title, m_msgWait.toUtf8()));
    QTC_ASSERT(diffEditor, return 0);
    diffEditor->document()->setProperty(registerDynamicProperty, dynamicPropertyValue);
jkobus's avatar
jkobus committed
1047
    VcsBasePlugin::setSource(diffEditor, source);
1048 1049 1050 1051

    Core::EditorManager::activateEditor(diffEditor);
    return diffEditor;
}
1052

con's avatar
con committed
1053 1054 1055 1056
/* 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
1057 1058 1059 1060 1061 1062 1063 1064
VcsBase::VcsBaseEditorWidget *GitClient::createVcsEditor(
        const Core::Id &id,
        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
1065
{
hjk's avatar
hjk committed
1066
    VcsBase::VcsBaseEditorWidget *rc = 0;
1067
    QTC_CHECK(!findExistingVCSEditor(registerDynamicProperty, dynamicPropertyValue));
1068 1069

    // Create new, set wait message, set up with source and codec
1070 1071
    Core::IEditor *outputEditor = Core::EditorManager::openEditorWithContents(id, &title,
                                                                              m_msgWait.toUtf8());
1072
    outputEditor->document()->setProperty(registerDynamicProperty, dynamicPropertyValue);
hjk's avatar
hjk committed
1073
    rc = VcsBase::VcsBaseEditorWidget::getVcsBaseEditor(outputEditor);
1074 1075
    connect(rc, SIGNAL(annotateRevisionRequested(QString,QString,QString,int)),
            this, SLOT(slotBlameRevisionRequested(QString,QString,QString,int)));
1076 1077
    QTC_ASSERT(rc, return 0);
    rc->setSource(source);
1078
    if (codecType == CodecSource) {
1079
        rc->setCodec(getSourceCodec(source));
1080 1081 1082 1083 1084 1085
    } else if (codecType == CodecLogOutput) {
        QString encodingName = readConfigValue(source, QLatin1String("i18n.logOutputEncoding"));
        if (encodingName.isEmpty())
            encodingName = QLatin1String("utf-8");
        rc->setCodec(QTextCodec::codecForName(encodingName.toLocal8Bit()));
    }