/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: 2017-07-20 00:00:04 UTC
  • mfrom: (6690.5.2 bundle-guess)
  • Revision ID: jelmer@jelmer.uk-20170720000004-wlknc5gthdk3tokn
Merge lp:~jelmer/brz/bundle-guess.

Show diffs side-by-side

added added

removed removed

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