giteditor.cpp 13.5 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 "giteditor.h"
hjk's avatar
hjk committed
31

con's avatar
con committed
32 33
#include "annotationhighlighter.h"
#include "gitplugin.h"
34
#include "gitclient.h"
35
#include "gitsettings.h"
36 37
#include "gitsubmiteditorwidget.h"
#include "gitconstants.h"
38
#include "githighlighters.h"
con's avatar
con committed
39

40
#include <coreplugin/icore.h>
hjk's avatar
hjk committed
41
#include <utils/qtcassert.h>
42
#include <vcsbase/vcsbaseoutputwindow.h>
43
#include <texteditor/basetextdocument.h>
44

45 46 47
#include <QFileInfo>
#include <QRegExp>
#include <QSet>
48
#include <QTemporaryFile>
49
#include <QTextCodec>
50
#include <QDir>
hjk's avatar
hjk committed
51

52 53
#include <QTextCursor>
#include <QTextBlock>
54
#include <QMessageBox>
con's avatar
con committed
55

56
#define CHANGE_PATTERN "[a-f0-9]{7,40}"
con's avatar
con committed
57 58 59 60

namespace Git {
namespace Internal {

61
GitEditorWidget::GitEditorWidget(const VcsBase::VcsBaseEditorParameters *type, QWidget *parent)  :
hjk's avatar
hjk committed
62
    VcsBase::VcsBaseEditorWidget(type, parent),
63
    m_changeNumberPattern(QLatin1String(CHANGE_PATTERN))
con's avatar
con committed
64
{
65
    QTC_ASSERT(m_changeNumberPattern.isValid(), return);
66 67 68 69 70 71 72
    /* Diff format:
        diff --git a/src/plugins/git/giteditor.cpp b/src/plugins/git/giteditor.cpp
        index 40997ff..4e49337 100644
        --- a/src/plugins/git/giteditor.cpp
        +++ b/src/plugins/git/giteditor.cpp
    */
    setDiffFilePattern(QRegExp(QLatin1String("^(?:diff --git a/|index |[+-]{3} (?:/dev/null|[ab]/(.+$)))")));
73
    setLogEntryPattern(QRegExp(QLatin1String("^commit ([0-9a-f]{8})[0-9a-f]{32}")));
74
    setAnnotateRevisionTextFormat(tr("Blame %1"));
75
    setAnnotatePreviousRevisionTextFormat(tr("Blame Parent Revision %1"));
con's avatar
con committed
76 77
}

78
QSet<QString> GitEditorWidget::annotationChanges() const
con's avatar
con committed
79 80 81 82 83 84
{
    QSet<QString> changes;
    const QString txt = toPlainText();
    if (txt.isEmpty())
        return changes;
    // Hunt for first change number in annotation: "<change>:"
85
    QRegExp r(QLatin1String("^(" CHANGE_PATTERN ") "));
hjk's avatar
hjk committed
86
    QTC_ASSERT(r.isValid(), return changes);
con's avatar
con committed
87 88
    if (r.indexIn(txt) != -1) {
        changes.insert(r.cap(1));
89
        r.setPattern(QLatin1String("\n(" CHANGE_PATTERN ") "));
hjk's avatar
hjk committed
90
        QTC_ASSERT(r.isValid(), return changes);
con's avatar
con committed
91 92 93 94 95 96 97 98 99
        int pos = 0;
        while ((pos = r.indexIn(txt, pos)) != -1) {
            pos += r.matchedLength();
            changes.insert(r.cap(1));
        }
    }
    return changes;
}

100
QString GitEditorWidget::changeUnderCursor(const QTextCursor &c) const
con's avatar
con committed
101 102 103 104 105 106 107
{
    QTextCursor cursor = c;
    // Any number is regarded as change number.
    cursor.select(QTextCursor::WordUnderCursor);
    if (!cursor.hasSelection())
        return QString();
    const QString change = cursor.selectedText();
108
    if (m_changeNumberPattern.exactMatch(change))
con's avatar
con committed
109 110 111 112
        return change;
    return QString();
}

113
VcsBase::BaseAnnotationHighlighter *GitEditorWidget::createAnnotationHighlighter(const QSet<QString> &changes) const
con's avatar
con committed
114
{
115
    return new GitAnnotationHighlighter(changes);
con's avatar
con committed
116 117
}

118 119 120 121 122
/* Remove the date specification from annotation, which is tabular:
\code
8ca887aa (author               YYYY-MM-DD HH:MM:SS <offset>  <line>)<content>
\endcode */

Orgad Shaneh's avatar
Orgad Shaneh committed
123
static QString removeAnnotationDate(const QString &b)
124
{
125
    if (b.isEmpty())
Orgad Shaneh's avatar
Orgad Shaneh committed
126
        return b;
127

Orgad Shaneh's avatar
Orgad Shaneh committed
128 129
    const QChar space(QLatin1Char(' '));
    const int parenPos = b.indexOf(QLatin1Char(')'));
130
    if (parenPos == -1)
Orgad Shaneh's avatar
Orgad Shaneh committed
131
        return b;
132 133
    int datePos = parenPos;

Orgad Shaneh's avatar
Orgad Shaneh committed
134
    int i = parenPos;
Orgad Shaneh's avatar
Orgad Shaneh committed
135
    while (i >= 0 && b.at(i) != space)
Orgad Shaneh's avatar
Orgad Shaneh committed
136
        --i;
Orgad Shaneh's avatar
Orgad Shaneh committed
137
    while (i >= 0 && b.at(i) == space)
Orgad Shaneh's avatar
Orgad Shaneh committed
138
        --i;
139
    int spaceCount = 0;
Orgad Shaneh's avatar
Orgad Shaneh committed
140 141
    // i is now on timezone. Go back 3 spaces: That is where the date starts.
    while (i >= 0) {
Orgad Shaneh's avatar
Orgad Shaneh committed
142
        if (b.at(i) == space)
143
            ++spaceCount;
Orgad Shaneh's avatar
Orgad Shaneh committed
144 145
        if (spaceCount == 3) {
            datePos = i;
146 147
            break;
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
148
        --i;
149 150
    }
    if (datePos == 0)
Orgad Shaneh's avatar
Orgad Shaneh committed
151
        return b;
152 153

    // Copy over the parts that have not changed into a new byte array
Orgad Shaneh's avatar
Orgad Shaneh committed
154
    QString result;
155
    QTC_ASSERT(b.size() >= parenPos, return result);
156
    int prevPos = 0;
Orgad Shaneh's avatar
Orgad Shaneh committed
157
    int pos = b.indexOf(QLatin1Char('\n'), 0) + 1;
158
    forever {
159
        QTC_CHECK(prevPos < pos);
Orgad Shaneh's avatar
Orgad Shaneh committed
160
        int afterParen = prevPos + parenPos;
Orgad Shaneh's avatar
Orgad Shaneh committed
161 162
        result.append(b.mid(prevPos, datePos));
        result.append(b.mid(afterParen, pos - afterParen));
163
        prevPos = pos;
164
        QTC_CHECK(prevPos != 0);
165
        if (pos == b.size())
166
            break;
167

Orgad Shaneh's avatar
Orgad Shaneh committed
168
        pos = b.indexOf(QLatin1Char('\n'), pos) + 1;
Orgad Shaneh's avatar
Orgad Shaneh committed
169
        if (pos == 0) // indexOf returned -1
170
            pos = b.size();
171
    }
172
    return result;
173 174
}

175
void GitEditorWidget::setPlainTextFiltered(const QString &text)
176
{
177
    QString modText = text;
178
    GitPlugin *plugin = GitPlugin::instance();
179
    // If desired, filter out the date from annotation
180 181 182 183 184
    switch (contentType())
    {
    case VcsBase::AnnotateOutput: {
        const bool omitAnnotationDate = plugin->settings().boolValue(GitSettings::omitAnnotationDateKey);
        if (omitAnnotationDate)
185
            modText = removeAnnotationDate(text);
186 187
        break;
    }
Tobias Hunger's avatar
Tobias Hunger committed
188 189
    default:
        break;
190 191
    }

192
    textDocument()->setPlainText(modText);
193 194
}

195
void GitEditorWidget::commandFinishedGotoLine(bool ok, int exitCode, const QVariant &v)
196
{
197
    reportCommandFinished(ok, exitCode, v);
198 199 200 201 202 203 204
    if (ok && v.type() == QVariant::Int) {
        const int line = v.toInt();
        if (line >= 0)
            gotoLine(line);
    }
}

205
void GitEditorWidget::checkoutChange()
206
{
207 208
    GitPlugin::instance()->gitClient()->stashAndCheckout(
                sourceWorkingDirectory(), m_currentChange);
209 210
}

211
void GitEditorWidget::resetChange()
212
{
213
    const QString workingDir = sourceWorkingDirectory();
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228

    GitClient *client = GitPlugin::instance()->gitClient();
    if (client->gitStatus(workingDir, StatusMode(NoUntracked | NoSubmodules))
            != GitClient::StatusUnchanged) {
        if (QMessageBox::question(
                    Core::ICore::mainWindow(), tr("Reset"),
                    tr("All changes in working directory will be discarded. Are you sure?"),
                    QMessageBox::Yes | QMessageBox::No,
                    QMessageBox::No) == QMessageBox::No) {
            return;
        }
    }
    client->reset(workingDir, QLatin1String("--hard"), m_currentChange);
}

229
void GitEditorWidget::cherryPickChange()
230
{
231 232
    GitPlugin::instance()->gitClient()->synchronousCherryPick(
                sourceWorkingDirectory(), m_currentChange);
233 234
}

235
void GitEditorWidget::revertChange()
236
{
237 238
    GitPlugin::instance()->gitClient()->synchronousRevert(
                sourceWorkingDirectory(), m_currentChange);
239 240
}

241
void GitEditorWidget::stageDiffChunk()
242 243 244 245 246 247 248
{
    const QAction *a = qobject_cast<QAction *>(sender());
    QTC_ASSERT(a, return);
    const VcsBase::DiffChunk chunk = qvariant_cast<VcsBase::DiffChunk>(a->data());
    return applyDiffChunk(chunk, false);
}

249
void GitEditorWidget::unstageDiffChunk()
250 251 252 253 254 255 256
{
    const QAction *a = qobject_cast<QAction *>(sender());
    QTC_ASSERT(a, return);
    const VcsBase::DiffChunk chunk = qvariant_cast<VcsBase::DiffChunk>(a->data());
    return applyDiffChunk(chunk, true);
}

257
void GitEditorWidget::applyDiffChunk(const VcsBase::DiffChunk& chunk, bool revert)
258 259 260 261 262 263
{
    VcsBase::VcsBaseOutputWindow *outwin = VcsBase::VcsBaseOutputWindow::instance();
    QTemporaryFile patchFile;
    if (!patchFile.open())
        return;

264
    const QString baseDir = workingDirectory();
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
    patchFile.write(chunk.header);
    patchFile.write(chunk.chunk);
    patchFile.close();

    GitClient *client = GitPlugin::instance()->gitClient();
    QStringList args = QStringList() << QLatin1String("--cached");
    if (revert)
        args << QLatin1String("--reverse");
    QString errorMessage;
    if (client->synchronousApplyPatch(baseDir, patchFile.fileName(), &errorMessage, args)) {
        if (errorMessage.isEmpty())
            outwin->append(tr("Chunk successfully staged"));
        else
            outwin->append(errorMessage);
        if (revert)
            emit diffChunkReverted(chunk);
        else
            emit diffChunkApplied(chunk);
    } else {
        outwin->appendError(errorMessage);
    }
}

288
void GitEditorWidget::init()
289 290
{
    VcsBase::VcsBaseEditorWidget::init();
291
    Core::Id editorId = editor()->document()->id();
292
    if (editorId == Git::Constants::GIT_COMMIT_TEXT_EDITOR_ID)
293
        new GitSubmitHighlighter(textDocument());
294
    else if (editorId == Git::Constants::GIT_REBASE_EDITOR_ID)
295
        new GitRebaseHighlighter(textDocument());
296 297
}

298
void GitEditorWidget::addDiffActions(QMenu *menu, const VcsBase::DiffChunk &chunk)
299 300 301 302 303 304 305 306 307 308 309 310
{
    menu->addSeparator();

    QAction *stageAction = menu->addAction(tr("Stage Chunk..."));
    stageAction->setData(qVariantFromValue(chunk));
    connect(stageAction, SIGNAL(triggered()), this, SLOT(stageDiffChunk()));

    QAction *unstageAction = menu->addAction(tr("Unstage Chunk..."));
    unstageAction->setData(qVariantFromValue(chunk));
    connect(unstageAction, SIGNAL(triggered()), this, SLOT(unstageDiffChunk()));
}

311
bool GitEditorWidget::open(QString *errorString, const QString &fileName, const QString &realFileName)
312
{
313
    Core::Id editorId = editor()->document()->id();
314 315
    if (editorId == Git::Constants::GIT_COMMIT_TEXT_EDITOR_ID
            || editorId == Git::Constants::GIT_REBASE_EDITOR_ID) {
316
        QFileInfo fi(fileName);
317 318
        const QString gitPath = fi.absolutePath();
        setSource(gitPath);
319
        textDocument()->setCodec(
320
                    GitPlugin::instance()->gitClient()->encoding(gitPath, "i18n.commitEncoding"));
321
    }
322
    return VcsBaseEditorWidget::open(errorString, fileName, realFileName);
323 324
}

325
QString GitEditorWidget::decorateVersion(const QString &revision) const
326 327 328 329 330 331 332 333
{
    const QFileInfo fi(source());
    const QString workingDirectory = fi.absolutePath();

    // Format verbose, SHA1 being first token
    return GitPlugin::instance()->gitClient()->synchronousShortDescription(workingDirectory, revision);
}

334
QStringList GitEditorWidget::annotationPreviousVersions(const QString &revision) const
335 336 337 338 339 340 341 342 343
{
    QStringList revisions;
    QString errorMessage;
    GitClient *client = GitPlugin::instance()->gitClient();
    const QFileInfo fi(source());
    const QString workingDirectory = fi.absolutePath();
    // Get the SHA1's of the file.
    if (!client->synchronousParentRevisions(workingDirectory, QStringList(fi.fileName()),
                                            revision, &revisions, &errorMessage)) {
hjk's avatar
hjk committed
344
        VcsBase::VcsBaseOutputWindow::instance()->appendSilently(errorMessage);
345 346
        return QStringList();
    }
347
    return revisions;
348 349
}

350
bool GitEditorWidget::isValidRevision(const QString &revision) const
351 352 353 354
{
    return GitPlugin::instance()->gitClient()->isValidRevision(revision);
}

355
void GitEditorWidget::addChangeActions(QMenu *menu, const QString &change)
356 357
{
    m_currentChange = change;
358 359 360
    if (contentType() != VcsBase::OtherContent) {
        menu->addAction(tr("Cherry-Pick Change %1").arg(change), this, SLOT(cherryPickChange()));
        menu->addAction(tr("Revert Change %1").arg(change), this, SLOT(revertChange()));
361 362
        menu->addAction(tr("Checkout Change %1").arg(change), this, SLOT(checkoutChange()));
        menu->addAction(tr("Hard Reset to Change %1").arg(change), this, SLOT(resetChange()));
363
    }
364 365
}

366
QString GitEditorWidget::revisionSubject(const QTextBlock &inBlock) const
367 368 369 370 371 372 373 374 375 376 377
{
    for (QTextBlock block = inBlock.next(); block.isValid(); block = block.next()) {
        const QString line = block.text().trimmed();
        if (line.isEmpty()) {
            block = block.next();
            return block.text().trimmed();
        }
    }
    return QString();
}

378
bool GitEditorWidget::supportChangeLinks() const
379 380
{
    return VcsBaseEditorWidget::supportChangeLinks()
381 382
            || (editor()->document()->id() == Git::Constants::GIT_COMMIT_TEXT_EDITOR_ID)
            || (editor()->document()->id() == Git::Constants::GIT_REBASE_EDITOR_ID);
383 384
}

385
QString GitEditorWidget::fileNameForLine(int line) const
386 387 388 389 390 391 392 393 394 395 396 397 398
{
    // 7971b6e7 share/qtcreator/dumper/dumper.py   (hjk
    QTextBlock block = document()->findBlockByLineNumber(line - 1);
    QTC_ASSERT(block.isValid(), return source());
    static QRegExp renameExp(QLatin1String("^" CHANGE_PATTERN "\\s+([^(]+)"));
    if (renameExp.indexIn(block.text()) != -1) {
        const QString fileName = renameExp.cap(1).trimmed();
        if (!fileName.isEmpty())
            return fileName;
    }
    return source();
}

399
QString GitEditorWidget::sourceWorkingDirectory() const
400 401 402 403 404
{
    const QFileInfo fi(source());
    return fi.isDir() ? fi.absoluteFilePath() : fi.absolutePath();
}

hjk's avatar
hjk committed
405 406
} // namespace Internal
} // namespace Git