/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/grep.py

  • Committer: Robert Collins
  • Date: 2007-04-19 02:27:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2426.
  • Revision ID: robertc@robertcollins.net-20070419022744-pfdqz42kp1wizh43
``make docs`` now creates a man page at ``man1/bzr.1`` fixing bug 107388.
(Robert Collins)

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