/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

[merge] robertc's integration, updated tests to check for retcode=3

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