~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Aaron Bentley
  • Date: 2006-05-16 15:39:35 UTC
  • mto: (1185.82.108 w-changeset)
  • mto: This revision was merged to the branch mainline in revision 1738.
  • Revision ID: abentley@panoramicfeedback.com-20060516153935-d41041f31ed315e2
Downgrade inventory mismatch to warning (source can be inaccurate)

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