/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/plugins/grep/grep.py

  • Committer: Jelmer Vernooij
  • Date: 2018-07-08 14:45:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7036.
  • Revision ID: jelmer@jelmer.uk-20180708144527-codhlvdcdg9y0nji
Fix a bunch of merge tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2010 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
from __future__ import absolute_import
 
18
 
 
19
import re
 
20
import sys
 
21
 
 
22
from ...lazy_import import lazy_import
 
23
lazy_import(globals(), """
 
24
from fnmatch import fnmatch
 
25
 
 
26
from breezy._termcolor import color_string, FG
 
27
 
 
28
from breezy import (
 
29
    controldir,
 
30
    diff,
 
31
    errors,
 
32
    lazy_regex,
 
33
    revision as _mod_revision,
 
34
    )
 
35
""")
 
36
from breezy import (
 
37
    osutils,
 
38
    trace,
 
39
    )
 
40
from breezy.revisionspec import (
 
41
    RevisionSpec,
 
42
    RevisionSpec_revid,
 
43
    RevisionSpec_revno,
 
44
    )
 
45
from breezy.sixish import (
 
46
    BytesIO,
 
47
    )
 
48
 
 
49
_user_encoding = osutils.get_user_encoding()
 
50
 
 
51
 
 
52
class _RevisionNotLinear(Exception):
 
53
    """Raised when a revision is not on left-hand history."""
 
54
 
 
55
 
 
56
def _rev_on_mainline(rev_tuple):
 
57
    """returns True is rev tuple is on mainline"""
 
58
    if len(rev_tuple) == 1:
 
59
        return True
 
60
    return rev_tuple[1] == 0 and rev_tuple[2] == 0
 
61
 
 
62
 
 
63
# NOTE: _linear_view_revisions is basided on
 
64
# breezy.log._linear_view_revisions.
 
65
# This should probably be a common public API
 
66
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
67
    # requires that start is older than end
 
68
    repo = branch.repository
 
69
    graph = repo.get_graph()
 
70
    for revision_id in graph.iter_lefthand_ancestry(
 
71
            end_rev_id, (_mod_revision.NULL_REVISION, )):
 
72
        revno = branch.revision_id_to_dotted_revno(revision_id)
 
73
        revno_str = '.'.join(str(n) for n in revno)
 
74
        if revision_id == start_rev_id:
 
75
            yield revision_id, revno_str, 0
 
76
            break
 
77
        yield revision_id, revno_str, 0
 
78
 
 
79
 
 
80
# NOTE: _graph_view_revisions is copied from
 
81
# breezy.log._graph_view_revisions.
 
82
# This should probably be a common public API
 
83
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
84
                          rebase_initial_depths=True):
 
85
    """Calculate revisions to view including merges, newest to oldest.
 
86
 
 
87
    :param branch: the branch
 
88
    :param start_rev_id: the lower revision-id
 
89
    :param end_rev_id: the upper revision-id
 
90
    :param rebase_initial_depth: should depths be rebased until a mainline
 
91
      revision is found?
 
92
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
93
    """
 
94
    # requires that start is older than end
 
95
    view_revisions = branch.iter_merge_sorted_revisions(
 
96
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
 
97
        stop_rule="with-merges")
 
98
    if not rebase_initial_depths:
 
99
        for (rev_id, merge_depth, revno, end_of_merge
 
100
             ) in view_revisions:
 
101
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
102
    else:
 
103
        # We're following a development line starting at a merged revision.
 
104
        # We need to adjust depths down by the initial depth until we find
 
105
        # a depth less than it. Then we use that depth as the adjustment.
 
106
        # If and when we reach the mainline, depth adjustment ends.
 
107
        depth_adjustment = None
 
108
        for (rev_id, merge_depth, revno, end_of_merge
 
109
             ) in view_revisions:
 
110
            if depth_adjustment is None:
 
111
                depth_adjustment = merge_depth
 
112
            if depth_adjustment:
 
113
                if merge_depth < depth_adjustment:
 
114
                    # From now on we reduce the depth adjustement, this can be
 
115
                    # surprising for users. The alternative requires two passes
 
116
                    # which breaks the fast display of the first revision
 
117
                    # though.
 
118
                    depth_adjustment = merge_depth
 
119
                merge_depth -= depth_adjustment
 
120
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
121
 
 
122
 
 
123
def compile_pattern(pattern, flags=0):
 
124
    patternc = None
 
125
    try:
 
126
        # use python's re.compile as we need to catch re.error in case of bad pattern
 
127
        lazy_regex.reset_compile()
 
128
        patternc = re.compile(pattern, flags)
 
129
    except re.error as e:
 
130
        raise errors.BzrError("Invalid pattern: '%s'" % pattern)
 
131
    return patternc
 
132
 
 
133
 
 
134
def is_fixed_string(s):
 
135
    if re.match("^([A-Za-z0-9_]|\s)*$", s):
 
136
        return True
 
137
    return False
 
138
 
 
139
 
 
140
class _GrepDiffOutputter(object):
 
141
    """Precalculate formatting based on options given for diff grep.
 
142
    """
 
143
 
 
144
    def __init__(self, opts):
 
145
        self.opts = opts
 
146
        self.outf = opts.outf
 
147
        if opts.show_color:
 
148
            pat = opts.pattern.encode(_user_encoding, 'replace')
 
149
            if opts.fixed_string:
 
150
                self._old = pat
 
151
                self._new = color_string(pat, FG.BOLD_RED)
 
152
                self.get_writer = self._get_writer_fixed_highlighted
 
153
            else:
 
154
                flags = opts.patternc.flags
 
155
                self._sub = re.compile(pat.join(("((?:", ")+)")), flags).sub
 
156
                self._highlight = color_string("\\1", FG.BOLD_RED)
 
157
                self.get_writer = self._get_writer_regexp_highlighted
 
158
        else:
 
159
            self.get_writer = self._get_writer_plain
 
160
 
 
161
    def get_file_header_writer(self):
 
162
        """Get function for writing file headers"""
 
163
        write = self.outf.write
 
164
        eol_marker = self.opts.eol_marker
 
165
        def _line_writer(line):
 
166
            write(line + eol_marker)
 
167
        def _line_writer_color(line):
 
168
            write(FG.BOLD_MAGENTA + line + FG.NONE + eol_marker)
 
169
        if self.opts.show_color:
 
170
            return _line_writer_color
 
171
        else:
 
172
            return _line_writer
 
173
        return _line_writer
 
174
 
 
175
    def get_revision_header_writer(self):
 
176
        """Get function for writing revno lines"""
 
177
        write = self.outf.write
 
178
        eol_marker = self.opts.eol_marker
 
179
        def _line_writer(line):
 
180
            write(line + eol_marker)
 
181
        def _line_writer_color(line):
 
182
            write(FG.BOLD_BLUE + line + FG.NONE + eol_marker)
 
183
        if self.opts.show_color:
 
184
            return _line_writer_color
 
185
        else:
 
186
            return _line_writer
 
187
        return _line_writer
 
188
 
 
189
    def _get_writer_plain(self):
 
190
        """Get function for writing uncoloured output"""
 
191
        write = self.outf.write
 
192
        eol_marker = self.opts.eol_marker
 
193
        def _line_writer(line):
 
194
            write(line + eol_marker)
 
195
        return _line_writer
 
196
 
 
197
    def _get_writer_regexp_highlighted(self):
 
198
        """Get function for writing output with regexp match highlighted"""
 
199
        _line_writer = self._get_writer_plain()
 
200
        sub, highlight = self._sub, self._highlight
 
201
        def _line_writer_regexp_highlighted(line):
 
202
            """Write formatted line with matched pattern highlighted"""
 
203
            return _line_writer(line=sub(highlight, line))
 
204
        return _line_writer_regexp_highlighted
 
205
 
 
206
    def _get_writer_fixed_highlighted(self):
 
207
        """Get function for writing output with search string highlighted"""
 
208
        _line_writer = self._get_writer_plain()
 
209
        old, new = self._old, self._new
 
210
        def _line_writer_fixed_highlighted(line):
 
211
            """Write formatted line with string searched for highlighted"""
 
212
            return _line_writer(line=line.replace(old, new))
 
213
        return _line_writer_fixed_highlighted
 
214
 
 
215
 
 
216
def grep_diff(opts):
 
217
    wt, branch, relpath = \
 
218
        controldir.ControlDir.open_containing_tree_or_branch('.')
 
219
    with branch.lock_read():
 
220
        if opts.revision:
 
221
            start_rev = opts.revision[0]
 
222
        else:
 
223
            # if no revision is sepcified for diff grep we grep all changesets.
 
224
            opts.revision = [RevisionSpec.from_string('revno:1'),
 
225
                RevisionSpec.from_string('last:1')]
 
226
            start_rev = opts.revision[0]
 
227
        start_revid = start_rev.as_revision_id(branch)
 
228
        if start_revid == b'null:':
 
229
            return
 
230
        srevno_tuple = branch.revision_id_to_dotted_revno(start_revid)
 
231
        if len(opts.revision) == 2:
 
232
            end_rev = opts.revision[1]
 
233
            end_revid = end_rev.as_revision_id(branch)
 
234
            if end_revid is None:
 
235
                end_revno, end_revid = branch.last_revision_info()
 
236
            erevno_tuple = branch.revision_id_to_dotted_revno(end_revid)
 
237
 
 
238
            grep_mainline = (_rev_on_mainline(srevno_tuple) and
 
239
                _rev_on_mainline(erevno_tuple))
 
240
 
 
241
            # ensure that we go in reverse order
 
242
            if srevno_tuple > erevno_tuple:
 
243
                srevno_tuple, erevno_tuple = erevno_tuple, srevno_tuple
 
244
                start_revid, end_revid = end_revid, start_revid
 
245
 
 
246
            # Optimization: Traversing the mainline in reverse order is much
 
247
            # faster when we don't want to look at merged revs. We try this
 
248
            # with _linear_view_revisions. If all revs are to be grepped we
 
249
            # use the slower _graph_view_revisions
 
250
            if opts.levels==1 and grep_mainline:
 
251
                given_revs = _linear_view_revisions(branch, start_revid, end_revid)
 
252
            else:
 
253
                given_revs = _graph_view_revisions(branch, start_revid, end_revid)
 
254
        else:
 
255
            # We do an optimization below. For grepping a specific revison
 
256
            # We don't need to call _graph_view_revisions which is slow.
 
257
            # We create the start_rev_tuple for only that specific revision.
 
258
            # _graph_view_revisions is used only for revision range.
 
259
            start_revno = '.'.join(map(str, srevno_tuple))
 
260
            start_rev_tuple = (start_revid, start_revno, 0)
 
261
            given_revs = [start_rev_tuple]
 
262
        repo = branch.repository
 
263
        diff_pattern = re.compile("^[+\-].*(" + opts.pattern + ")")
 
264
        file_pattern = re.compile("=== (modified|added|removed) file '.*'", re.UNICODE)
 
265
        outputter = _GrepDiffOutputter(opts)
 
266
        writeline = outputter.get_writer()
 
267
        writerevno = outputter.get_revision_header_writer()
 
268
        writefileheader = outputter.get_file_header_writer()
 
269
        file_encoding = _user_encoding
 
270
        for revid, revno, merge_depth in given_revs:
 
271
            if opts.levels == 1 and merge_depth != 0:
 
272
                # with level=1 show only top level
 
273
                continue
 
274
 
 
275
            rev_spec = RevisionSpec_revid.from_string("revid:"+revid)
 
276
            new_rev = repo.get_revision(revid)
 
277
            new_tree = rev_spec.as_tree(branch)
 
278
            if len(new_rev.parent_ids) == 0:
 
279
                ancestor_id = _mod_revision.NULL_REVISION
 
280
            else:
 
281
                ancestor_id = new_rev.parent_ids[0]
 
282
            old_tree = repo.revision_tree(ancestor_id)
 
283
            s = BytesIO()
 
284
            diff.show_diff_trees(old_tree, new_tree, s,
 
285
                old_label='', new_label='')
 
286
            display_revno = True
 
287
            display_file = False
 
288
            file_header = None
 
289
            text = s.getvalue()
 
290
            for line in text.splitlines():
 
291
                if file_pattern.search(line):
 
292
                    file_header = line
 
293
                    display_file = True
 
294
                elif diff_pattern.search(line):
 
295
                    if display_revno:
 
296
                        writerevno("=== revno:%s ===" % (revno,))
 
297
                        display_revno = False
 
298
                    if display_file:
 
299
                        writefileheader("  %s" % (file_header,))
 
300
                        display_file = False
 
301
                    line = line.decode(file_encoding, 'replace')
 
302
                    writeline("    %s" % (line,))
 
303
 
 
304
 
 
305
def versioned_grep(opts):
 
306
    wt, branch, relpath = \
 
307
        controldir.ControlDir.open_containing_tree_or_branch('.')
 
308
    with branch.lock_read():
 
309
        start_rev = opts.revision[0]
 
310
        start_revid = start_rev.as_revision_id(branch)
 
311
        if start_revid is None:
 
312
            start_rev = RevisionSpec_revno.from_string("revno:1")
 
313
            start_revid = start_rev.as_revision_id(branch)
 
314
        srevno_tuple = branch.revision_id_to_dotted_revno(start_revid)
 
315
 
 
316
        if len(opts.revision) == 2:
 
317
            end_rev = opts.revision[1]
 
318
            end_revid = end_rev.as_revision_id(branch)
 
319
            if end_revid is None:
 
320
                end_revno, end_revid = branch.last_revision_info()
 
321
            erevno_tuple = branch.revision_id_to_dotted_revno(end_revid)
 
322
 
 
323
            grep_mainline = (_rev_on_mainline(srevno_tuple) and
 
324
                _rev_on_mainline(erevno_tuple))
 
325
 
 
326
            # ensure that we go in reverse order
 
327
            if srevno_tuple > erevno_tuple:
 
328
                srevno_tuple, erevno_tuple = erevno_tuple, srevno_tuple
 
329
                start_revid, end_revid = end_revid, start_revid
 
330
 
 
331
            # Optimization: Traversing the mainline in reverse order is much
 
332
            # faster when we don't want to look at merged revs. We try this
 
333
            # with _linear_view_revisions. If all revs are to be grepped we
 
334
            # use the slower _graph_view_revisions
 
335
            if opts.levels == 1 and grep_mainline:
 
336
                given_revs = _linear_view_revisions(branch, start_revid, end_revid)
 
337
            else:
 
338
                given_revs = _graph_view_revisions(branch, start_revid, end_revid)
 
339
        else:
 
340
            # We do an optimization below. For grepping a specific revison
 
341
            # We don't need to call _graph_view_revisions which is slow.
 
342
            # We create the start_rev_tuple for only that specific revision.
 
343
            # _graph_view_revisions is used only for revision range.
 
344
            start_revno = '.'.join(map(str, srevno_tuple))
 
345
            start_rev_tuple = (start_revid, start_revno, 0)
 
346
            given_revs = [start_rev_tuple]
 
347
 
 
348
        # GZ 2010-06-02: Shouldn't be smuggling this on opts, but easy for now
 
349
        opts.outputter = _Outputter(opts, use_cache=True)
 
350
 
 
351
        for revid, revno, merge_depth in given_revs:
 
352
            if opts.levels == 1 and merge_depth != 0:
 
353
                # with level=1 show only top level
 
354
                continue
 
355
 
 
356
            rev = RevisionSpec_revid.from_string("revid:"+revid)
 
357
            tree = rev.as_tree(branch)
 
358
            for path in opts.path_list:
 
359
                tree_path = osutils.pathjoin(relpath, path)
 
360
                if not tree.has_filename(tree_path):
 
361
                    trace.warning("Skipped unknown file '%s'." % path)
 
362
                    continue
 
363
 
 
364
                if osutils.isdir(path):
 
365
                    path_prefix = path
 
366
                    dir_grep(tree, path, relpath, opts, revno, path_prefix)
 
367
                else:
 
368
                    versioned_file_grep(tree, tree_path, '.', path, opts, revno)
 
369
 
 
370
 
 
371
def workingtree_grep(opts):
 
372
    revno = opts.print_revno = None # for working tree set revno to None
 
373
 
 
374
    tree, branch, relpath = \
 
375
        controldir.ControlDir.open_containing_tree_or_branch('.')
 
376
    if not tree:
 
377
        msg = ('Cannot search working tree. Working tree not found.\n'
 
378
            'To search for specific revision in history use the -r option.')
 
379
        raise errors.BzrCommandError(msg)
 
380
 
 
381
    # GZ 2010-06-02: Shouldn't be smuggling this on opts, but easy for now
 
382
    opts.outputter = _Outputter(opts)
 
383
 
 
384
    with tree.lock_read():
 
385
        for path in opts.path_list:
 
386
            if osutils.isdir(path):
 
387
                path_prefix = path
 
388
                dir_grep(tree, path, relpath, opts, revno, path_prefix)
 
389
            else:
 
390
                _file_grep(open(path).read(), path, opts, revno)
 
391
 
 
392
 
 
393
def _skip_file(include, exclude, path):
 
394
    if include and not _path_in_glob_list(path, include):
 
395
        return True
 
396
    if exclude and _path_in_glob_list(path, exclude):
 
397
        return True
 
398
    return False
 
399
 
 
400
 
 
401
def dir_grep(tree, path, relpath, opts, revno, path_prefix):
 
402
    # setup relpath to open files relative to cwd
 
403
    rpath = relpath
 
404
    if relpath:
 
405
        rpath = osutils.pathjoin('..', relpath)
 
406
 
 
407
    from_dir = osutils.pathjoin(relpath, path)
 
408
    if opts.from_root:
 
409
        # start searching recursively from root
 
410
        from_dir = None
 
411
        recursive = True
 
412
 
 
413
    to_grep = []
 
414
    to_grep_append = to_grep.append
 
415
    # GZ 2010-06-05: The cache dict used to be recycled every call to dir_grep
 
416
    #                and hits manually refilled. Could do this again if it was
 
417
    #                for a good reason, otherwise cache might want purging.
 
418
    outputter = opts.outputter
 
419
    for fp, fc, fkind, fid, entry in tree.list_files(include_root=False,
 
420
        from_dir=from_dir, recursive=opts.recursive):
 
421
 
 
422
        if _skip_file(opts.include, opts.exclude, fp):
 
423
            continue
 
424
 
 
425
        if fc == 'V' and fkind == 'file':
 
426
            tree_path = osutils.pathjoin(from_dir if from_dir else '', fp)
 
427
            if revno is not None:
 
428
                # If old result is valid, print results immediately.
 
429
                # Otherwise, add file info to to_grep so that the
 
430
                # loop later will get chunks and grep them
 
431
                cache_id = tree.get_file_revision(tree_path, fid)
 
432
                if cache_id in outputter.cache:
 
433
                    # GZ 2010-06-05: Not really sure caching and re-outputting
 
434
                    #                the old path is really the right thing,
 
435
                    #                but it's what the old code seemed to do
 
436
                    outputter.write_cached_lines(cache_id, revno)
 
437
                else:
 
438
                    to_grep_append((tree_path, (fp, tree_path)))
 
439
            else:
 
440
                # we are grepping working tree.
 
441
                if from_dir is None:
 
442
                    from_dir = '.'
 
443
 
 
444
                path_for_file = osutils.pathjoin(tree.basedir, from_dir, fp)
 
445
                if opts.files_with_matches or opts.files_without_match:
 
446
                    # Optimize for wtree list-only as we don't need to read the
 
447
                    # entire file
 
448
                    file = open(path_for_file, 'r', buffering=4096)
 
449
                    _file_grep_list_only_wtree(file, fp, opts, path_prefix)
 
450
                else:
 
451
                    file_text = open(path_for_file, 'r').read()
 
452
                    _file_grep(file_text, fp, opts, revno, path_prefix)
 
453
 
 
454
    if revno is not None: # grep versioned files
 
455
        for (path, tree_path), chunks in tree.iter_files_bytes(to_grep):
 
456
            path = _make_display_path(relpath, path)
 
457
            _file_grep(''.join(chunks), path, opts, revno, path_prefix,
 
458
                tree.get_file_revision(tree_path))
 
459
 
 
460
 
 
461
def _make_display_path(relpath, path):
 
462
    """Return path string relative to user cwd.
 
463
 
 
464
    Take tree's 'relpath' and user supplied 'path', and return path
 
465
    that can be displayed to the user.
 
466
    """
 
467
    if relpath:
 
468
        # update path so to display it w.r.t cwd
 
469
        # handle windows slash separator
 
470
        path = osutils.normpath(osutils.pathjoin(relpath, path))
 
471
        path = path.replace('\\', '/')
 
472
        path = path.replace(relpath + '/', '', 1)
 
473
    return path
 
474
 
 
475
 
 
476
def versioned_file_grep(tree, tree_path, relpath, path, opts, revno, path_prefix = None):
 
477
    """Create a file object for the specified id and pass it on to _file_grep.
 
478
    """
 
479
 
 
480
    path = _make_display_path(relpath, path)
 
481
    file_text = tree.get_file_text(tree_path)
 
482
    _file_grep(file_text, path, opts, revno, path_prefix)
 
483
 
 
484
 
 
485
def _path_in_glob_list(path, glob_list):
 
486
    for glob in glob_list:
 
487
        if fnmatch(path, glob):
 
488
            return True
 
489
    return False
 
490
 
 
491
 
 
492
def _file_grep_list_only_wtree(file, path, opts, path_prefix=None):
 
493
    # test and skip binary files
 
494
    if '\x00' in file.read(1024):
 
495
        if opts.verbose:
 
496
            trace.warning("Binary file '%s' skipped." % path)
 
497
        return
 
498
 
 
499
    file.seek(0) # search from beginning
 
500
 
 
501
    found = False
 
502
    if opts.fixed_string:
 
503
        pattern = opts.pattern.encode(_user_encoding, 'replace')
 
504
        for line in file:
 
505
            if pattern in line:
 
506
                found = True
 
507
                break
 
508
    else: # not fixed_string
 
509
        for line in file:
 
510
            if opts.patternc.search(line):
 
511
                found = True
 
512
                break
 
513
 
 
514
    if (opts.files_with_matches and found) or \
 
515
        (opts.files_without_match and not found):
 
516
        if path_prefix and path_prefix != '.':
 
517
            # user has passed a dir arg, show that as result prefix
 
518
            path = osutils.pathjoin(path_prefix, path)
 
519
        opts.outputter.get_writer(path, None, None)()
 
520
 
 
521
 
 
522
class _Outputter(object):
 
523
    """Precalculate formatting based on options given
 
524
 
 
525
    The idea here is to do this work only once per run, and finally return a
 
526
    function that will do the minimum amount possible for each match.
 
527
    """
 
528
    def __init__(self, opts, use_cache=False):
 
529
        self.outf = opts.outf
 
530
        if use_cache:
 
531
            # self.cache is used to cache results for dir grep based on fid.
 
532
            # If the fid is does not change between results, it means that
 
533
            # the result will be the same apart from revno. In such a case
 
534
            # we avoid getting file chunks from repo and grepping. The result
 
535
            # is just printed by replacing old revno with new one.
 
536
            self.cache = {}
 
537
        else:
 
538
            self.cache = None
 
539
        no_line = opts.files_with_matches or opts.files_without_match
 
540
 
 
541
        if opts.show_color:
 
542
            pat = opts.pattern.encode(_user_encoding, 'replace')
 
543
            if no_line:
 
544
                self.get_writer = self._get_writer_plain
 
545
            elif opts.fixed_string:
 
546
                self._old = pat
 
547
                self._new = color_string(pat, FG.BOLD_RED)
 
548
                self.get_writer = self._get_writer_fixed_highlighted
 
549
            else:
 
550
                flags = opts.patternc.flags
 
551
                self._sub = re.compile(pat.join(("((?:", ")+)")), flags).sub
 
552
                self._highlight = color_string("\\1", FG.BOLD_RED)
 
553
                self.get_writer = self._get_writer_regexp_highlighted
 
554
            path_start = FG.MAGENTA
 
555
            path_end = FG.NONE
 
556
            sep = color_string(':', FG.BOLD_CYAN)
 
557
            rev_sep = color_string('~', FG.BOLD_YELLOW)
 
558
        else:
 
559
            self.get_writer = self._get_writer_plain
 
560
            path_start = path_end = ""
 
561
            sep = ":"
 
562
            rev_sep = "~"
 
563
 
 
564
        parts = [path_start, "%(path)s"]
 
565
        if opts.print_revno:
 
566
            parts.extend([rev_sep, "%(revno)s"])
 
567
        self._format_initial = "".join(parts)
 
568
        parts = []
 
569
        if no_line:
 
570
            if not opts.print_revno:
 
571
                parts.append(path_end)
 
572
        else:
 
573
            if opts.line_number:
 
574
                parts.extend([sep, "%(lineno)s"])
 
575
            parts.extend([sep, "%(line)s"])
 
576
        parts.append(opts.eol_marker)
 
577
        self._format_perline = "".join(parts)
 
578
 
 
579
    def _get_writer_plain(self, path, revno, cache_id):
 
580
        """Get function for writing uncoloured output"""
 
581
        per_line = self._format_perline
 
582
        start = self._format_initial % {"path":path, "revno":revno}
 
583
        write = self.outf.write
 
584
        if self.cache is not None and cache_id is not None:
 
585
            result_list = []
 
586
            self.cache[cache_id] = path, result_list
 
587
            add_to_cache = result_list.append
 
588
            def _line_cache_and_writer(**kwargs):
 
589
                """Write formatted line and cache arguments"""
 
590
                end = per_line % kwargs
 
591
                add_to_cache(end)
 
592
                write(start + end)
 
593
            return _line_cache_and_writer
 
594
        def _line_writer(**kwargs):
 
595
            """Write formatted line from arguments given by underlying opts"""
 
596
            write(start + per_line % kwargs)
 
597
        return _line_writer
 
598
 
 
599
    def write_cached_lines(self, cache_id, revno):
 
600
        """Write cached results out again for new revision"""
 
601
        cached_path, cached_matches = self.cache[cache_id]
 
602
        start = self._format_initial % {"path":cached_path, "revno":revno}
 
603
        write = self.outf.write
 
604
        for end in cached_matches:
 
605
            write(start + end)
 
606
 
 
607
    def _get_writer_regexp_highlighted(self, path, revno, cache_id):
 
608
        """Get function for writing output with regexp match highlighted"""
 
609
        _line_writer = self._get_writer_plain(path, revno, cache_id)
 
610
        sub, highlight = self._sub, self._highlight
 
611
        def _line_writer_regexp_highlighted(line, **kwargs):
 
612
            """Write formatted line with matched pattern highlighted"""
 
613
            return _line_writer(line=sub(highlight, line), **kwargs)
 
614
        return _line_writer_regexp_highlighted
 
615
 
 
616
    def _get_writer_fixed_highlighted(self, path, revno, cache_id):
 
617
        """Get function for writing output with search string highlighted"""
 
618
        _line_writer = self._get_writer_plain(path, revno, cache_id)
 
619
        old, new = self._old, self._new
 
620
        def _line_writer_fixed_highlighted(line, **kwargs):
 
621
            """Write formatted line with string searched for highlighted"""
 
622
            return _line_writer(line=line.replace(old, new), **kwargs)
 
623
        return _line_writer_fixed_highlighted
 
624
 
 
625
 
 
626
def _file_grep(file_text, path, opts, revno, path_prefix=None, cache_id=None):
 
627
    # test and skip binary files
 
628
    if b'\x00' in file_text[:1024]:
 
629
        if opts.verbose:
 
630
            trace.warning("Binary file '%s' skipped." % path)
 
631
        return
 
632
 
 
633
    if path_prefix and path_prefix != '.':
 
634
        # user has passed a dir arg, show that as result prefix
 
635
        path = osutils.pathjoin(path_prefix, path)
 
636
 
 
637
    # GZ 2010-06-07: There's no actual guarentee the file contents will be in
 
638
    #                the user encoding, but we have to guess something and it
 
639
    #                is a reasonable default without a better mechanism.
 
640
    file_encoding = _user_encoding
 
641
    pattern = opts.pattern.encode(_user_encoding, 'replace')
 
642
 
 
643
    writeline = opts.outputter.get_writer(path, revno, cache_id)
 
644
 
 
645
    if opts.files_with_matches or opts.files_without_match:
 
646
        if opts.fixed_string:
 
647
            found = pattern in file_text
 
648
        else:
 
649
            search = opts.patternc.search
 
650
            if "$" not in pattern:
 
651
                found = search(file_text) is not None
 
652
            else:
 
653
                for line in file_text.splitlines():
 
654
                    if search(line):
 
655
                        found = True
 
656
                        break
 
657
                else:
 
658
                    found = False
 
659
        if (opts.files_with_matches and found) or \
 
660
                (opts.files_without_match and not found):
 
661
            writeline()
 
662
    elif opts.fixed_string:
 
663
        # Fast path for no match, search through the entire file at once rather
 
664
        # than a line at a time. <http://effbot.org/zone/stringlib.htm>
 
665
        i = file_text.find(pattern)
 
666
        if i == -1:
 
667
            return
 
668
        b = file_text.rfind("\n", 0, i) + 1
 
669
        if opts.line_number:
 
670
            start = file_text.count("\n", 0, b) + 1
 
671
        file_text = file_text[b:]
 
672
        if opts.line_number:
 
673
            for index, line in enumerate(file_text.splitlines()):
 
674
                if pattern in line:
 
675
                    line = line.decode(file_encoding, 'replace')
 
676
                    writeline(lineno=index+start, line=line)
 
677
        else:
 
678
            for line in file_text.splitlines():
 
679
                if pattern in line:
 
680
                    line = line.decode(file_encoding, 'replace')
 
681
                    writeline(line=line)
 
682
    else:
 
683
        # Fast path on no match, the re module avoids bad behaviour in most
 
684
        # standard cases, but perhaps could try and detect backtracking
 
685
        # patterns here and avoid whole text search in those cases
 
686
        search = opts.patternc.search
 
687
        if "$" not in pattern:
 
688
            # GZ 2010-06-05: Grr, re.MULTILINE can't save us when searching
 
689
            #                through revisions as bazaar returns binary mode
 
690
            #                and trailing \r breaks $ as line ending match
 
691
            m = search(file_text)
 
692
            if m is None:
 
693
                return
 
694
            b = file_text.rfind("\n", 0, m.start()) + 1
 
695
            if opts.line_number:
 
696
                start = file_text.count("\n", 0, b) + 1
 
697
            file_text = file_text[b:]
 
698
        else:
 
699
            start = 1
 
700
        if opts.line_number:
 
701
            for index, line in enumerate(file_text.splitlines()):
 
702
                if search(line):
 
703
                    line = line.decode(file_encoding, 'replace')
 
704
                    writeline(lineno=index+start, line=line)
 
705
        else:
 
706
            for line in file_text.splitlines():
 
707
                if search(line):
 
708
                    line = line.decode(file_encoding, 'replace')
 
709
                    writeline(line=line)
 
710