gitgrep.cpp 10.9 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
28
29
/****************************************************************************
**
** 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"
#include "gitplugin.h"

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

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

#include <QFuture>
#include <QFutureWatcher>
47
48
#include <QHBoxLayout>
#include <QRegularExpressionValidator>
Orgad Shaneh's avatar
Orgad Shaneh committed
49
#include <QScopedPointer>
Orgad Shaneh's avatar
Orgad Shaneh committed
50
#include <QSettings>
Orgad Shaneh's avatar
Orgad Shaneh committed
51
#include <QTextStream>
Orgad Shaneh's avatar
Orgad Shaneh committed
52
53
54
55

namespace Git {
namespace Internal {

56
57
58
59
60
61
class GitGrepParameters
{
public:
    QString ref;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
62
using namespace Core;
Orgad Shaneh's avatar
Orgad Shaneh committed
63
using namespace Utils;
Orgad Shaneh's avatar
Orgad Shaneh committed
64
using VcsBase::VcsCommand;
Orgad Shaneh's avatar
Orgad Shaneh committed
65
66
67

namespace {

68
const char GitGrepRef[] = "GitGrepRef";
69

Orgad Shaneh's avatar
Orgad Shaneh committed
70
71
72
73
74
75
76
77
78
79
80
81
82
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
83
    void processLine(const QString &line, FileSearchResultList *resultList) const
Orgad Shaneh's avatar
Orgad Shaneh committed
84
85
86
    {
        if (line.isEmpty())
            return;
Orgad Shaneh's avatar
Orgad Shaneh committed
87
88
        static const QLatin1String boldRed("\x1b[1;31m");
        static const QLatin1String resetColor("\x1b[m");
Orgad Shaneh's avatar
Orgad Shaneh committed
89
        FileSearchResult single;
Orgad Shaneh's avatar
Orgad Shaneh committed
90
        const int lineSeparator = line.indexOf(QChar::Null);
91
92
93
        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
94
        single.fileName = m_directory + '/' + filePath;
Orgad Shaneh's avatar
Orgad Shaneh committed
95
        const int textSeparator = line.indexOf(QChar::Null, lineSeparator + 1);
Orgad Shaneh's avatar
Orgad Shaneh committed
96
        single.lineNumber = line.mid(lineSeparator + 1, textSeparator - lineSeparator - 1).toInt();
Orgad Shaneh's avatar
Orgad Shaneh committed
97
        QString text = line.mid(textSeparator + 1);
Orgad Shaneh's avatar
Orgad Shaneh committed
98
99
100
101
102
        QVector<QPair<int, int>> matches;
        for (;;) {
            const int matchStart = text.indexOf(boldRed);
            if (matchStart == -1)
                break;
Orgad Shaneh's avatar
Orgad Shaneh committed
103
            const int matchTextStart = matchStart + boldRed.size();
Orgad Shaneh's avatar
Orgad Shaneh committed
104
105
106
107
108
            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
109
                    + text.mid(matchEnd + resetColor.size());
Orgad Shaneh's avatar
Orgad Shaneh committed
110
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
111
        single.matchingLine = text;
112
113
114
115
116
117
118
119
120

        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
121
122
123
124
125
126
127
        foreach (auto match, matches) {
            single.matchStart = match.first;
            single.matchLength = match.second;
            resultList->append(single);
        }
    }

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

    void exec()
    {
        QStringList arguments;
Orgad Shaneh's avatar
Orgad Shaneh committed
142
143
144
145
        arguments << "-c" << "color.grep.match=bold red"
                  << "grep" << "-zn"
                  << "--no-full-name"
                  << "--color=always";
Orgad Shaneh's avatar
Orgad Shaneh committed
146
        if (!(m_parameters.flags & FindCaseSensitively))
Orgad Shaneh's avatar
Orgad Shaneh committed
147
            arguments << "-i";
Orgad Shaneh's avatar
Orgad Shaneh committed
148
        if (m_parameters.flags & FindWholeWords)
Orgad Shaneh's avatar
Orgad Shaneh committed
149
            arguments << "-w";
Orgad Shaneh's avatar
Orgad Shaneh committed
150
        if (m_parameters.flags & FindRegularExpression)
Orgad Shaneh's avatar
Orgad Shaneh committed
151
            arguments << "-P";
Orgad Shaneh's avatar
Orgad Shaneh committed
152
        else
Orgad Shaneh's avatar
Orgad Shaneh committed
153
            arguments << "-F";
Orgad Shaneh's avatar
Orgad Shaneh committed
154
        arguments << m_parameters.text;
155
        GitGrepParameters params = m_parameters.searchEngineParameters.value<GitGrepParameters>();
156
157
        if (!params.ref.isEmpty()) {
            arguments << params.ref;
Orgad Shaneh's avatar
Orgad Shaneh committed
158
            m_ref = params.ref + ':';
159
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
160
        arguments << "--" << m_parameters.nameFilters;
Tobias Hunger's avatar
Tobias Hunger committed
161
        QScopedPointer<VcsCommand> command(GitPlugin::client()->createCommand(m_directory));
162
        command->addFlags(VcsCommand::SilentOutput | VcsCommand::SuppressFailMessage);
Orgad Shaneh's avatar
Orgad Shaneh committed
163
        command->setProgressiveOutput(true);
Orgad Shaneh's avatar
Orgad Shaneh committed
164
165
166
        QFutureWatcher<FileSearchResultList> watcher;
        watcher.setFuture(m_fi.future());
        connect(&watcher, &QFutureWatcher<FileSearchResultList>::canceled,
Orgad Shaneh's avatar
Orgad Shaneh committed
167
168
                command.data(), &VcsCommand::cancel);
        connect(command.data(), &VcsCommand::stdOutText, this, &GitGrepRunner::read);
Tobias Hunger's avatar
Tobias Hunger committed
169
        SynchronousProcessResponse resp = command->runCommand(GitPlugin::client()->vcsBinary(), arguments, 0);
170
171
172
173
        switch (resp.result) {
        case SynchronousProcessResponse::TerminatedAbnormally:
        case SynchronousProcessResponse::StartFailed:
        case SynchronousProcessResponse::Hang:
Orgad Shaneh's avatar
Orgad Shaneh committed
174
            m_fi.reportCanceled();
175
176
177
178
179
180
181
            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
182
183
184
185
186
187
    }

    static void run(QFutureInterface<FileSearchResultList> &fi,
                    TextEditor::FileFindParameters parameters)
    {
        GitGrepRunner runner(fi, parameters);
188
        Core::ProgressTimer progress(fi, 5);
Orgad Shaneh's avatar
Orgad Shaneh committed
189
190
191
192
193
194
        runner.exec();
    }

private:
    FutureInterfaceType m_fi;
    QString m_directory;
195
    QString m_ref;
Orgad Shaneh's avatar
Orgad Shaneh committed
196
197
198
199
200
    const TextEditor::FileFindParameters &m_parameters;
};

} // namespace

201
static bool isGitDirectory(const QString &path)
Orgad Shaneh's avatar
Orgad Shaneh committed
202
{
203
204
205
    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
206
207
}

208
GitGrep::GitGrep()
Orgad Shaneh's avatar
Orgad Shaneh committed
209
{
210
211
212
213
    m_widget = new QWidget;
    auto layout = new QHBoxLayout(m_widget);
    layout->setMargin(0);
    m_treeLineEdit = new FancyLineEdit;
214
215
216
    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
217
    const QRegularExpression refExpression("[\\S]*");
218
219
    m_treeLineEdit->setValidator(new QRegularExpressionValidator(refExpression, this));
    layout->addWidget(m_treeLineEdit);
220
221
    TextEditor::FindInFiles *findInFiles = TextEditor::FindInFiles::instance();
    QTC_ASSERT(findInFiles, return);
Orgad Shaneh's avatar
Orgad Shaneh committed
222
223
    connect(findInFiles, &TextEditor::FindInFiles::pathChanged,
            m_widget, [this](const QString &path) {
224
        setEnabled(isGitDirectory(path));
225
    });
226
    connect(this, &SearchEngine::enabledChanged, m_widget, &QWidget::setEnabled);
227
    findInFiles->addSearchEngine(this);
Orgad Shaneh's avatar
Orgad Shaneh committed
228
229
}

230
GitGrep::~GitGrep()
Orgad Shaneh's avatar
Orgad Shaneh committed
231
{
Orgad Shaneh's avatar
Orgad Shaneh committed
232
    delete m_widget;
Orgad Shaneh's avatar
Orgad Shaneh committed
233
234
}

235
QString GitGrep::title() const
Orgad Shaneh's avatar
Orgad Shaneh committed
236
{
237
    return tr("Git Grep");
Orgad Shaneh's avatar
Orgad Shaneh committed
238
239
}

240
241
242
243
244
245
246
247
QString GitGrep::toolTip() const
{
    const QString ref = m_treeLineEdit->text();
    if (!ref.isEmpty())
        return tr("Ref: %1\n%2").arg(ref);
    return QLatin1String("%1");
}

248
QWidget *GitGrep::widget() const
Orgad Shaneh's avatar
Orgad Shaneh committed
249
{
Orgad Shaneh's avatar
Orgad Shaneh committed
250
    return m_widget;
Orgad Shaneh's avatar
Orgad Shaneh committed
251
252
}

253
QVariant GitGrep::parameters() const
Orgad Shaneh's avatar
Orgad Shaneh committed
254
{
255
256
257
    GitGrepParameters params;
    params.ref = m_treeLineEdit->text();
    return qVariantFromValue(params);
Orgad Shaneh's avatar
Orgad Shaneh committed
258
259
260
261
}

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

265
void GitGrep::writeSettings(QSettings *settings) const
Orgad Shaneh's avatar
Orgad Shaneh committed
266
{
Orgad Shaneh's avatar
Orgad Shaneh committed
267
    settings->setValue(GitGrepRef, m_treeLineEdit->text());
Orgad Shaneh's avatar
Orgad Shaneh committed
268
269
}

270
271
QFuture<FileSearchResultList> GitGrep::executeSearch(const TextEditor::FileFindParameters &parameters,
        TextEditor::BaseFileFind * /*baseFileFind*/)
Orgad Shaneh's avatar
Orgad Shaneh committed
272
{
273
    return Utils::runAsync(GitGrepRunner::run, parameters);
Orgad Shaneh's avatar
Orgad Shaneh committed
274
275
}

276
277
278
IEditor *GitGrep::openEditor(const SearchResultItem &item,
                             const TextEditor::FileFindParameters &parameters)
{
279
280
    GitGrepParameters params = parameters.searchEngineParameters.value<GitGrepParameters>();
    if (params.ref.isEmpty() || item.path.isEmpty())
281
282
283
284
285
        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
286
    if (!GitPlugin::client()->synchronousShow(topLevel, params.ref + ":./" + relativePath,
Tobias Hunger's avatar
Tobias Hunger committed
287
                                              &content, nullptr)) {
288
289
290
291
292
293
294
295
296
297
298
299
        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
    }
    QString title = tr("Git Show %1:%2").arg(params.ref).arg(relativePath);
    IEditor *editor = EditorManager::openEditorWithContents(Id(), &title, content, title,
                                                            EditorManager::DoNotSwitchToDesignMode);
300
    editor->gotoLine(item.mainRange.begin.line, item.mainRange.begin.column);
301
302
303
304
    editor->document()->setTemporary(true);
    return editor;
}

Orgad Shaneh's avatar
Orgad Shaneh committed
305
306
} // Internal
} // Git
307
308

Q_DECLARE_METATYPE(Git::Internal::GitGrepParameters)