gitgrep.cpp 11.6 KB
Newer Older
Orgad Shaneh's avatar
Orgad Shaneh committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/****************************************************************************
**
** Copyright (C) 2016 Orgad Shaneh <orgads@gmail.com>.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** 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 The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "gitgrep.h"
#include "gitclient.h"
28
#include "gitconstants.h"
Orgad Shaneh's avatar
Orgad Shaneh committed
29
30
#include "gitplugin.h"

31
#include <coreplugin/editormanager/editormanager.h>
32
#include <coreplugin/progressmanager/progressmanager.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
33
#include <coreplugin/vcsmanager.h>
34
#include <texteditor/findinfiles.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
35
#include <vcsbase/vcscommand.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
36
37
#include <vcsbase/vcsbaseconstants.h>

38
#include <utils/algorithm.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
39
#include <utils/asconst.h>
40
#include <utils/fancylineedit.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
41
42
43
44
#include <utils/filesearch.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
#include <utils/runextensions.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
45
#include <utils/synchronousprocess.h>
46
#include <utils/textfileformat.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
47
48
49

#include <QFuture>
#include <QFutureWatcher>
50
51
#include <QHBoxLayout>
#include <QRegularExpressionValidator>
Orgad Shaneh's avatar
Orgad Shaneh committed
52
#include <QScopedPointer>
Orgad Shaneh's avatar
Orgad Shaneh committed
53
#include <QSettings>
Orgad Shaneh's avatar
Orgad Shaneh committed
54
#include <QTextStream>
Orgad Shaneh's avatar
Orgad Shaneh committed
55
56
57
58

namespace Git {
namespace Internal {

59
60
61
62
63
64
class GitGrepParameters
{
public:
    QString ref;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
65
using namespace Core;
Orgad Shaneh's avatar
Orgad Shaneh committed
66
using namespace Utils;
Orgad Shaneh's avatar
Orgad Shaneh committed
67
using VcsBase::VcsCommand;
Orgad Shaneh's avatar
Orgad Shaneh committed
68
69
70

namespace {

71
const char GitGrepRef[] = "GitGrepRef";
72

Orgad Shaneh's avatar
Orgad Shaneh committed
73
74
75
76
77
78
79
80
81
82
83
84
85
class GitGrepRunner : public QObject
{
    using FutureInterfaceType = QFutureInterface<FileSearchResultList>;

public:
    GitGrepRunner(FutureInterfaceType &fi,
                  const TextEditor::FileFindParameters &parameters) :
        m_fi(fi),
        m_parameters(parameters)
    {
        m_directory = parameters.additionalParameters.toString();
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
86
    void processLine(const QString &line, FileSearchResultList *resultList) const
Orgad Shaneh's avatar
Orgad Shaneh committed
87
88
89
    {
        if (line.isEmpty())
            return;
Orgad Shaneh's avatar
Orgad Shaneh committed
90
91
        static const QLatin1String boldRed("\x1b[1;31m");
        static const QLatin1String resetColor("\x1b[m");
Orgad Shaneh's avatar
Orgad Shaneh committed
92
        FileSearchResult single;
Orgad Shaneh's avatar
Orgad Shaneh committed
93
        const int lineSeparator = line.indexOf(QChar::Null);
94
95
96
        QString filePath = line.left(lineSeparator);
        if (!m_ref.isEmpty() && filePath.startsWith(m_ref))
            filePath.remove(0, m_ref.length());
Orgad Shaneh's avatar
Orgad Shaneh committed
97
        single.fileName = m_directory + '/' + filePath;
Orgad Shaneh's avatar
Orgad Shaneh committed
98
        const int textSeparator = line.indexOf(QChar::Null, lineSeparator + 1);
Orgad Shaneh's avatar
Orgad Shaneh committed
99
        single.lineNumber = line.mid(lineSeparator + 1, textSeparator - lineSeparator - 1).toInt();
Orgad Shaneh's avatar
Orgad Shaneh committed
100
        QString text = line.mid(textSeparator + 1);
Orgad Shaneh's avatar
Orgad Shaneh committed
101
102
103
104
105
        QVector<QPair<int, int>> matches;
        for (;;) {
            const int matchStart = text.indexOf(boldRed);
            if (matchStart == -1)
                break;
Orgad Shaneh's avatar
Orgad Shaneh committed
106
            const int matchTextStart = matchStart + boldRed.size();
Orgad Shaneh's avatar
Orgad Shaneh committed
107
108
109
110
111
            const int matchEnd = text.indexOf(resetColor, matchTextStart);
            QTC_ASSERT(matchEnd != -1, break);
            const int matchLength = matchEnd - matchTextStart;
            matches.append(qMakePair(matchStart, matchLength));
            text = text.left(matchStart) + text.mid(matchTextStart, matchLength)
Orgad Shaneh's avatar
Orgad Shaneh committed
112
                    + text.mid(matchEnd + resetColor.size());
Orgad Shaneh's avatar
Orgad Shaneh committed
113
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
114
        single.matchingLine = text;
115
116
117
118
119
120
121
122
123

        if (m_parameters.flags & FindRegularExpression) {
            const QRegularExpression::PatternOptions patternOptions =
                    (m_parameters.flags & QTextDocument::FindCaseSensitively)
                    ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption;
            QRegularExpression regexp(m_parameters.text, patternOptions);
            QRegularExpressionMatch regexpMatch = regexp.match(line);
            single.regexpCapturedTexts = regexpMatch.capturedTexts();
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
124
        for (auto match : Utils::asConst(matches)) {
Orgad Shaneh's avatar
Orgad Shaneh committed
125
126
127
128
129
130
            single.matchStart = match.first;
            single.matchLength = match.second;
            resultList->append(single);
        }
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
131
    void read(const QString &text)
Orgad Shaneh's avatar
Orgad Shaneh committed
132
133
    {
        FileSearchResultList resultList;
Orgad Shaneh's avatar
Orgad Shaneh committed
134
135
136
137
        QString t = text;
        QTextStream stream(&t);
        while (!stream.atEnd() && !m_fi.isCanceled())
            processLine(stream.readLine(), &resultList);
Orgad Shaneh's avatar
Orgad Shaneh committed
138
139
140
141
142
143
144
        if (!resultList.isEmpty())
            m_fi.reportResult(resultList);
    }

    void exec()
    {
        QStringList arguments;
Orgad Shaneh's avatar
Orgad Shaneh committed
145
146
147
148
        arguments << "-c" << "color.grep.match=bold red"
                  << "grep" << "-zn"
                  << "--no-full-name"
                  << "--color=always";
Orgad Shaneh's avatar
Orgad Shaneh committed
149
        if (!(m_parameters.flags & FindCaseSensitively))
Orgad Shaneh's avatar
Orgad Shaneh committed
150
            arguments << "-i";
Orgad Shaneh's avatar
Orgad Shaneh committed
151
        if (m_parameters.flags & FindWholeWords)
Orgad Shaneh's avatar
Orgad Shaneh committed
152
            arguments << "-w";
Orgad Shaneh's avatar
Orgad Shaneh committed
153
        if (m_parameters.flags & FindRegularExpression)
Orgad Shaneh's avatar
Orgad Shaneh committed
154
            arguments << "-P";
Orgad Shaneh's avatar
Orgad Shaneh committed
155
        else
Orgad Shaneh's avatar
Orgad Shaneh committed
156
            arguments << "-F";
157
        arguments << "-e" << m_parameters.text;
158
        GitGrepParameters params = m_parameters.searchEngineParameters.value<GitGrepParameters>();
159
160
        if (!params.ref.isEmpty()) {
            arguments << params.ref;
Orgad Shaneh's avatar
Orgad Shaneh committed
161
            m_ref = params.ref + ':';
162
        }
163
164
165
166
167
168
169
170
        const QStringList filterArgs =
                m_parameters.nameFilters.isEmpty() ? QStringList("*") // needed for exclusion filters
                                                   : m_parameters.nameFilters;
        const QStringList exclusionArgs =
                Utils::transform(m_parameters.exclusionFilters, [](const QString &filter) {
                    return QString(":!" + filter);
                });
        arguments << "--" << filterArgs << exclusionArgs;
Tobias Hunger's avatar
Tobias Hunger committed
171
        QScopedPointer<VcsCommand> command(GitPlugin::client()->createCommand(m_directory));
172
        command->addFlags(VcsCommand::SilentOutput | VcsCommand::SuppressFailMessage);
Orgad Shaneh's avatar
Orgad Shaneh committed
173
        command->setProgressiveOutput(true);
Orgad Shaneh's avatar
Orgad Shaneh committed
174
175
176
        QFutureWatcher<FileSearchResultList> watcher;
        watcher.setFuture(m_fi.future());
        connect(&watcher, &QFutureWatcher<FileSearchResultList>::canceled,
Orgad Shaneh's avatar
Orgad Shaneh committed
177
178
                command.data(), &VcsCommand::cancel);
        connect(command.data(), &VcsCommand::stdOutText, this, &GitGrepRunner::read);
Tobias Hunger's avatar
Tobias Hunger committed
179
        SynchronousProcessResponse resp = command->runCommand(GitPlugin::client()->vcsBinary(), arguments, 0);
180
181
182
183
        switch (resp.result) {
        case SynchronousProcessResponse::TerminatedAbnormally:
        case SynchronousProcessResponse::StartFailed:
        case SynchronousProcessResponse::Hang:
Orgad Shaneh's avatar
Orgad Shaneh committed
184
            m_fi.reportCanceled();
185
186
187
188
189
190
191
            break;
        case SynchronousProcessResponse::Finished:
        case SynchronousProcessResponse::FinishedError:
            // When no results are found, git-grep exits with non-zero status.
            // Do not consider this as an error.
            break;
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
192
193
194
195
196
197
    }

    static void run(QFutureInterface<FileSearchResultList> &fi,
                    TextEditor::FileFindParameters parameters)
    {
        GitGrepRunner runner(fi, parameters);
198
        Core::ProgressTimer progress(fi, 5);
Orgad Shaneh's avatar
Orgad Shaneh committed
199
200
201
202
203
204
        runner.exec();
    }

private:
    FutureInterfaceType m_fi;
    QString m_directory;
205
    QString m_ref;
Orgad Shaneh's avatar
Orgad Shaneh committed
206
207
208
209
210
    const TextEditor::FileFindParameters &m_parameters;
};

} // namespace

211
static bool isGitDirectory(const QString &path)
Orgad Shaneh's avatar
Orgad Shaneh committed
212
{
213
214
215
    static IVersionControl *gitVc = VcsManager::versionControl(VcsBase::Constants::VCS_ID_GIT);
    QTC_ASSERT(gitVc, return false);
    return gitVc == VcsManager::findVersionControlForDirectory(path, 0);
Orgad Shaneh's avatar
Orgad Shaneh committed
216
217
}

218
GitGrep::GitGrep()
Orgad Shaneh's avatar
Orgad Shaneh committed
219
{
220
221
222
223
    m_widget = new QWidget;
    auto layout = new QHBoxLayout(m_widget);
    layout->setMargin(0);
    m_treeLineEdit = new FancyLineEdit;
224
225
226
    m_treeLineEdit->setPlaceholderText(tr("Tree (optional)"));
    m_treeLineEdit->setToolTip(tr("Can be HEAD, tag, local or remote branch, or a commit hash.\n"
                                  "Leave empty to search through the file system."));
Orgad Shaneh's avatar
Orgad Shaneh committed
227
    const QRegularExpression refExpression("[\\S]*");
228
229
    m_treeLineEdit->setValidator(new QRegularExpressionValidator(refExpression, this));
    layout->addWidget(m_treeLineEdit);
230
231
    TextEditor::FindInFiles *findInFiles = TextEditor::FindInFiles::instance();
    QTC_ASSERT(findInFiles, return);
Orgad Shaneh's avatar
Orgad Shaneh committed
232
233
    connect(findInFiles, &TextEditor::FindInFiles::pathChanged,
            m_widget, [this](const QString &path) {
234
        setEnabled(isGitDirectory(path));
235
    });
236
    connect(this, &SearchEngine::enabledChanged, m_widget, &QWidget::setEnabled);
237
    findInFiles->addSearchEngine(this);
Orgad Shaneh's avatar
Orgad Shaneh committed
238
239
}

240
GitGrep::~GitGrep()
Orgad Shaneh's avatar
Orgad Shaneh committed
241
{
Orgad Shaneh's avatar
Orgad Shaneh committed
242
    delete m_widget;
Orgad Shaneh's avatar
Orgad Shaneh committed
243
244
}

245
QString GitGrep::title() const
Orgad Shaneh's avatar
Orgad Shaneh committed
246
{
247
    return tr("Git Grep");
Orgad Shaneh's avatar
Orgad Shaneh committed
248
249
}

250
251
252
253
254
255
256
257
QString GitGrep::toolTip() const
{
    const QString ref = m_treeLineEdit->text();
    if (!ref.isEmpty())
        return tr("Ref: %1\n%2").arg(ref);
    return QLatin1String("%1");
}

258
QWidget *GitGrep::widget() const
Orgad Shaneh's avatar
Orgad Shaneh committed
259
{
Orgad Shaneh's avatar
Orgad Shaneh committed
260
    return m_widget;
Orgad Shaneh's avatar
Orgad Shaneh committed
261
262
}

263
QVariant GitGrep::parameters() const
Orgad Shaneh's avatar
Orgad Shaneh committed
264
{
265
266
267
    GitGrepParameters params;
    params.ref = m_treeLineEdit->text();
    return qVariantFromValue(params);
Orgad Shaneh's avatar
Orgad Shaneh committed
268
269
270
271
}

void GitGrep::readSettings(QSettings *settings)
{
Orgad Shaneh's avatar
Orgad Shaneh committed
272
    m_treeLineEdit->setText(settings->value(GitGrepRef).toString());
Orgad Shaneh's avatar
Orgad Shaneh committed
273
274
}

275
void GitGrep::writeSettings(QSettings *settings) const
Orgad Shaneh's avatar
Orgad Shaneh committed
276
{
Orgad Shaneh's avatar
Orgad Shaneh committed
277
    settings->setValue(GitGrepRef, m_treeLineEdit->text());
Orgad Shaneh's avatar
Orgad Shaneh committed
278
279
}

280
281
QFuture<FileSearchResultList> GitGrep::executeSearch(const TextEditor::FileFindParameters &parameters,
        TextEditor::BaseFileFind * /*baseFileFind*/)
Orgad Shaneh's avatar
Orgad Shaneh committed
282
{
283
    return Utils::runAsync(GitGrepRunner::run, parameters);
Orgad Shaneh's avatar
Orgad Shaneh committed
284
285
}

286
287
288
IEditor *GitGrep::openEditor(const SearchResultItem &item,
                             const TextEditor::FileFindParameters &parameters)
{
289
290
    GitGrepParameters params = parameters.searchEngineParameters.value<GitGrepParameters>();
    if (params.ref.isEmpty() || item.path.isEmpty())
291
292
293
294
295
        return nullptr;
    const QString path = QDir::fromNativeSeparators(item.path.first());
    QByteArray content;
    const QString topLevel = parameters.additionalParameters.toString();
    const QString relativePath = QDir(topLevel).relativeFilePath(path);
Orgad Shaneh's avatar
Orgad Shaneh committed
296
    if (!GitPlugin::client()->synchronousShow(topLevel, params.ref + ":./" + relativePath,
Tobias Hunger's avatar
Tobias Hunger committed
297
                                              &content, nullptr)) {
298
299
300
301
302
303
304
305
306
        return nullptr;
    }
    if (content.isEmpty())
        return nullptr;
    QByteArray fileContent;
    if (TextFileFormat::readFileUTF8(path, 0, &fileContent, 0) == TextFileFormat::ReadSuccess) {
        if (fileContent == content)
            return nullptr; // open the file for read/write
    }
307
308
309
310

    const QString documentId = QLatin1String(Git::Constants::GIT_PLUGIN)
            + QLatin1String(".GitShow.") + params.ref
            + QLatin1String(".") + relativePath;
311
    QString title = tr("Git Show %1:%2").arg(params.ref).arg(relativePath);
312
    IEditor *editor = EditorManager::openEditorWithContents(Id(), &title, content, documentId,
313
                                                            EditorManager::DoNotSwitchToDesignMode);
314
    editor->gotoLine(item.mainRange.begin.line, item.mainRange.begin.column);
315
316
317
318
    editor->document()->setTemporary(true);
    return editor;
}

Orgad Shaneh's avatar
Orgad Shaneh committed
319
320
} // Internal
} // Git
321
322

Q_DECLARE_METATYPE(Git::Internal::GitGrepParameters)