/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-11-18 18:23:32 UTC
  • mto: This revision was merged to the branch mainline in revision 7197.
  • Revision ID: jelmer@jelmer.uk-20181118182332-viz1qvqese2mo9i6
Fix some more Bazaar references.

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