~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Patch Queue Manager
  • Date: 2016-02-01 19:56:05 UTC
  • mfrom: (6615.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20160201195605-o7rl92wf6uyum3fk
(vila) Open trunk again as 2.8b1 (Vincent Ladeuil)

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