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

  • Committer: Breezy landing bot
  • Author(s): Colin Watson
  • Date: 2020-11-16 21:47:08 UTC
  • mfrom: (7521.1.1 remove-lp-workaround)
  • Revision ID: breezy.the.bot@gmail.com-20201116214708-jos209mgxi41oy15
Remove breezy.git workaround for bazaar.launchpad.net.

Merged from https://code.launchpad.net/~cjwatson/brz/remove-lp-workaround/+merge/393710

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
* in "verbose" mode with a description of what changed from one
25
25
  version to the next
26
26
 
27
 
* with file-ids and revision-ids shown
 
27
* with files and revision-ids shown
28
28
 
29
29
Logs are actually written out through an abstract LogFormatter
30
30
interface, which allows for different preferred formats.  Plugins can
47
47
all the changes since the previous revision that touched hello.c.
48
48
"""
49
49
 
50
 
from __future__ import absolute_import
51
 
 
52
50
import codecs
53
 
from cStringIO import StringIO
54
 
from itertools import (
55
 
    chain,
56
 
    izip,
57
 
    )
 
51
from io import BytesIO
 
52
import itertools
58
53
import re
59
54
import sys
60
55
from warnings import (
61
56
    warn,
62
57
    )
63
58
 
64
 
from bzrlib.lazy_import import lazy_import
 
59
from .lazy_import import lazy_import
65
60
lazy_import(globals(), """
66
61
 
67
 
from bzrlib import (
 
62
from breezy import (
68
63
    config,
69
64
    controldir,
70
65
    diff,
71
 
    errors,
72
66
    foreign,
73
 
    repository as _mod_repository,
 
67
    lazy_regex,
74
68
    revision as _mod_revision,
75
 
    revisionspec,
76
 
    tsort,
77
69
    )
78
 
from bzrlib.i18n import gettext, ngettext
 
70
from breezy.i18n import gettext, ngettext
79
71
""")
80
72
 
81
 
from bzrlib import (
82
 
    lazy_regex,
 
73
from . import (
 
74
    errors,
83
75
    registry,
 
76
    revisionspec,
 
77
    trace,
84
78
    )
85
 
from bzrlib.osutils import (
 
79
from .osutils import (
86
80
    format_date,
87
81
    format_date_with_offset_in_original_timezone,
88
82
    get_diff_header_encoding,
89
83
    get_terminal_encoding,
 
84
    is_inside,
90
85
    terminal_width,
91
86
    )
92
 
 
93
 
 
94
 
def find_touching_revisions(branch, file_id):
95
 
    """Yield a description of revisions which affect the file_id.
 
87
from .tree import (
 
88
    find_previous_path,
 
89
    InterTree,
 
90
    )
 
91
 
 
92
 
 
93
def find_touching_revisions(repository, last_revision, last_tree, last_path):
 
94
    """Yield a description of revisions which affect the file.
96
95
 
97
96
    Each returned element is (revno, revision_id, description)
98
97
 
102
101
    TODO: Perhaps some way to limit this to only particular revisions,
103
102
    or to traverse a non-mainline set of revisions?
104
103
    """
105
 
    last_ie = None
106
 
    last_path = None
107
 
    revno = 1
108
 
    graph = branch.repository.get_graph()
109
 
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
110
 
        [_mod_revision.NULL_REVISION]))
111
 
    for revision_id in reversed(history):
112
 
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if this_inv.has_id(file_id):
114
 
            this_ie = this_inv[file_id]
115
 
            this_path = this_inv.id2path(file_id)
116
 
        else:
117
 
            this_ie = this_path = None
 
104
    last_verifier = last_tree.get_file_verifier(last_path)
 
105
    graph = repository.get_graph()
 
106
    history = list(graph.iter_lefthand_ancestry(last_revision, []))
 
107
    revno = len(history)
 
108
    for revision_id in history:
 
109
        this_tree = repository.revision_tree(revision_id)
 
110
        this_intertree = InterTree.get(this_tree, last_tree)
 
111
        this_path = this_intertree.find_source_path(last_path)
118
112
 
119
113
        # now we know how it was last time, and how it is in this revision.
120
114
        # are those two states effectively the same or not?
121
 
 
122
 
        if not this_ie and not last_ie:
123
 
            # not present in either
124
 
            pass
125
 
        elif this_ie and not last_ie:
126
 
            yield revno, revision_id, "added " + this_path
127
 
        elif not this_ie and last_ie:
128
 
            # deleted here
129
 
            yield revno, revision_id, "deleted " + last_path
 
115
        if this_path is not None and last_path is None:
 
116
            yield revno, revision_id, "deleted " + this_path
 
117
            this_verifier = this_tree.get_file_verifier(this_path)
 
118
        elif this_path is None and last_path is not None:
 
119
            yield revno, revision_id, "added " + last_path
130
120
        elif this_path != last_path:
131
 
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
132
 
        elif (this_ie.text_size != last_ie.text_size
133
 
              or this_ie.text_sha1 != last_ie.text_sha1):
134
 
            yield revno, revision_id, "modified " + this_path
 
121
            yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
 
122
            this_verifier = this_tree.get_file_verifier(this_path)
 
123
        else:
 
124
            this_verifier = this_tree.get_file_verifier(this_path)
 
125
            if (this_verifier != last_verifier):
 
126
                yield revno, revision_id, "modified " + this_path
135
127
 
136
 
        last_ie = this_ie
 
128
        last_verifier = this_verifier
137
129
        last_path = this_path
138
 
        revno += 1
 
130
        last_tree = this_tree
 
131
        if last_path is None:
 
132
            return
 
133
        revno -= 1
139
134
 
140
135
 
141
136
def show_log(branch,
142
137
             lf,
143
 
             specific_fileid=None,
144
138
             verbose=False,
145
139
             direction='reverse',
146
140
             start_revision=None,
147
141
             end_revision=None,
148
 
             search=None,
149
142
             limit=None,
150
143
             show_diff=False,
151
144
             match=None):
158
151
 
159
152
    :param lf: The LogFormatter object showing the output.
160
153
 
161
 
    :param specific_fileid: If not None, list only the commits affecting the
162
 
        specified file, rather than all commits.
163
 
 
164
154
    :param verbose: If True show added/changed/deleted/renamed files.
165
155
 
166
156
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
170
160
 
171
161
    :param end_revision: If not None, only show revisions <= end_revision
172
162
 
173
 
    :param search: If not None, only show revisions with matching commit
174
 
        messages
175
 
 
176
163
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
177
164
        if None or 0.
178
165
 
181
168
    :param match: Dictionary of search lists to use when matching revision
182
169
      properties.
183
170
    """
184
 
    # Convert old-style parameters to new-style parameters
185
 
    if specific_fileid is not None:
186
 
        file_ids = [specific_fileid]
187
 
    else:
188
 
        file_ids = None
189
171
    if verbose:
190
 
        if file_ids:
191
 
            delta_type = 'partial'
192
 
        else:
193
 
            delta_type = 'full'
 
172
        delta_type = 'full'
194
173
    else:
195
174
        delta_type = None
196
175
    if show_diff:
197
 
        if file_ids:
198
 
            diff_type = 'partial'
199
 
        else:
200
 
            diff_type = 'full'
 
176
        diff_type = 'full'
201
177
    else:
202
178
        diff_type = None
203
179
 
 
180
    if isinstance(start_revision, int):
 
181
        try:
 
182
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
 
183
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
184
            raise errors.InvalidRevisionNumber(start_revision)
 
185
 
 
186
    if isinstance(end_revision, int):
 
187
        try:
 
188
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
 
189
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
190
            raise errors.InvalidRevisionNumber(end_revision)
 
191
 
 
192
    if end_revision is not None and end_revision.revno == 0:
 
193
        raise errors.InvalidRevisionNumber(end_revision.revno)
 
194
 
204
195
    # Build the request and execute it
205
 
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
 
196
    rqst = make_log_request_dict(
 
197
        direction=direction,
206
198
        start_revision=start_revision, end_revision=end_revision,
207
 
        limit=limit, message_search=search,
208
 
        delta_type=delta_type, diff_type=diff_type)
 
199
        limit=limit, delta_type=delta_type, diff_type=diff_type)
209
200
    Logger(branch, rqst).show(lf)
210
201
 
211
202
 
220
211
    }
221
212
 
222
213
 
223
 
def make_log_request_dict(direction='reverse', specific_fileids=None,
 
214
def make_log_request_dict(direction='reverse', specific_files=None,
224
215
                          start_revision=None, end_revision=None, limit=None,
225
216
                          message_search=None, levels=None, generate_tags=True,
226
217
                          delta_type=None,
237
228
    :param direction: 'reverse' (default) is latest to earliest;
238
229
      'forward' is earliest to latest.
239
230
 
240
 
    :param specific_fileids: If not None, only include revisions
 
231
    :param specific_files: If not None, only include revisions
241
232
      affecting the specified files, rather than all revisions.
242
233
 
243
234
    :param start_revision: If not None, only generate
260
251
`
261
252
    :param delta_type: Either 'full', 'partial' or None.
262
253
      'full' means generate the complete delta - adds/deletes/modifies/etc;
263
 
      'partial' means filter the delta using specific_fileids;
 
254
      'partial' means filter the delta using specific_files;
264
255
      None means do not generate any delta.
265
256
 
266
257
    :param diff_type: Either 'full', 'partial' or None.
267
258
      'full' means generate the complete diff - adds/deletes/modifies/etc;
268
 
      'partial' means filter the diff using specific_fileids;
 
259
      'partial' means filter the diff using specific_files;
269
260
      None means do not generate any diff.
270
261
 
271
262
    :param _match_using_deltas: a private parameter controlling the
272
 
      algorithm used for matching specific_fileids. This parameter
273
 
      may be removed in the future so bzrlib client code should NOT
 
263
      algorithm used for matching specific_files. This parameter
 
264
      may be removed in the future so breezy client code should NOT
274
265
      use it.
275
266
 
276
267
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
294
285
            else:
295
286
                match['message'] = [message_search]
296
287
        else:
297
 
            match={ 'message': [message_search] }
 
288
            match = {'message': [message_search]}
298
289
    return {
299
290
        'direction': direction,
300
 
        'specific_fileids': specific_fileids,
 
291
        'specific_files': specific_files,
301
292
        'start_revision': start_revision,
302
293
        'end_revision': end_revision,
303
294
        'limit': limit,
322
313
    return result
323
314
 
324
315
 
325
 
def format_signature_validity(rev_id, repo):
 
316
def format_signature_validity(rev_id, branch):
326
317
    """get the signature validity
327
318
 
328
319
    :param rev_id: revision id to validate
329
 
    :param repo: repository of revision
 
320
    :param branch: branch of revision
330
321
    :return: human readable string to print to log
331
322
    """
332
 
    from bzrlib import gpg
 
323
    from breezy import gpg
333
324
 
334
 
    gpg_strategy = gpg.GPGStrategy(None)
335
 
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
 
325
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
 
326
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
336
327
    if result[0] == gpg.SIGNATURE_VALID:
337
328
        return u"valid signature from {0}".format(result[1])
338
329
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
375
366
        if not isinstance(lf, LogFormatter):
376
367
            warn("not a LogFormatter instance: %r" % lf)
377
368
 
378
 
        self.branch.lock_read()
379
 
        try:
 
369
        with self.branch.lock_read():
380
370
            if getattr(lf, 'begin_log', None):
381
371
                lf.begin_log()
382
372
            self._show_body(lf)
383
373
            if getattr(lf, 'end_log', None):
384
374
                lf.end_log()
385
 
        finally:
386
 
            self.branch.unlock()
387
375
 
388
376
    def _show_body(self, lf):
389
377
        """Show the main log output.
408
396
 
409
397
        # Find and print the interesting revisions
410
398
        generator = self._generator_factory(self.branch, rqst)
411
 
        for lr in generator.iter_log_revisions():
412
 
            lf.log_revision(lr)
 
399
        try:
 
400
            for lr in generator.iter_log_revisions():
 
401
                lf.log_revision(lr)
 
402
        except errors.GhostRevisionUnusableHere:
 
403
            raise errors.CommandError(
 
404
                gettext('Further revision history missing.'))
413
405
        lf.show_advice()
414
406
 
415
407
    def _generator_factory(self, branch, rqst):
417
409
 
418
410
        Subclasses may wish to override this.
419
411
        """
420
 
        return _DefaultLogGenerator(branch, rqst)
 
412
        return _DefaultLogGenerator(branch, **rqst)
 
413
 
 
414
 
 
415
def _log_revision_iterator_using_per_file_graph(
 
416
        branch, delta_type, match, levels, path, start_rev_id, end_rev_id,
 
417
        direction, exclude_common_ancestry):
 
418
    # Get the base revisions, filtering by the revision range.
 
419
    # Note that we always generate the merge revisions because
 
420
    # filter_revisions_touching_path() requires them ...
 
421
    view_revisions = _calc_view_revisions(
 
422
        branch, start_rev_id, end_rev_id,
 
423
        direction, generate_merge_revisions=True,
 
424
        exclude_common_ancestry=exclude_common_ancestry)
 
425
    if not isinstance(view_revisions, list):
 
426
        view_revisions = list(view_revisions)
 
427
    view_revisions = _filter_revisions_touching_path(
 
428
        branch, path, view_revisions,
 
429
        include_merges=levels != 1)
 
430
    return make_log_rev_iterator(
 
431
        branch, view_revisions, delta_type, match)
 
432
 
 
433
 
 
434
def _log_revision_iterator_using_delta_matching(
 
435
        branch, delta_type, match, levels, specific_files, start_rev_id, end_rev_id,
 
436
        direction, exclude_common_ancestry, limit):
 
437
    # Get the base revisions, filtering by the revision range
 
438
    generate_merge_revisions = levels != 1
 
439
    delayed_graph_generation = not specific_files and (
 
440
        limit or start_rev_id or end_rev_id)
 
441
    view_revisions = _calc_view_revisions(
 
442
        branch, start_rev_id, end_rev_id,
 
443
        direction,
 
444
        generate_merge_revisions=generate_merge_revisions,
 
445
        delayed_graph_generation=delayed_graph_generation,
 
446
        exclude_common_ancestry=exclude_common_ancestry)
 
447
 
 
448
    # Apply the other filters
 
449
    return make_log_rev_iterator(branch, view_revisions,
 
450
                                 delta_type, match,
 
451
                                 files=specific_files,
 
452
                                 direction=direction)
 
453
 
 
454
 
 
455
def _format_diff(branch, rev, diff_type, files=None):
 
456
    """Format a diff.
 
457
 
 
458
    :param branch: Branch object
 
459
    :param rev: Revision object
 
460
    :param diff_type: Type of diff to generate
 
461
    :param files: List of files to generate diff for (or None for all)
 
462
    """
 
463
    repo = branch.repository
 
464
    if len(rev.parent_ids) == 0:
 
465
        ancestor_id = _mod_revision.NULL_REVISION
 
466
    else:
 
467
        ancestor_id = rev.parent_ids[0]
 
468
    tree_1 = repo.revision_tree(ancestor_id)
 
469
    tree_2 = repo.revision_tree(rev.revision_id)
 
470
    if diff_type == 'partial' and files is not None:
 
471
        specific_files = files
 
472
    else:
 
473
        specific_files = None
 
474
    s = BytesIO()
 
475
    path_encoding = get_diff_header_encoding()
 
476
    diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
477
                         new_label='', path_encoding=path_encoding)
 
478
    return s.getvalue()
421
479
 
422
480
 
423
481
class _StartNotLinearAncestor(Exception):
427
485
class _DefaultLogGenerator(LogGenerator):
428
486
    """The default generator of log revisions."""
429
487
 
430
 
    def __init__(self, branch, rqst):
 
488
    def __init__(
 
489
            self, branch, levels=None, limit=None, diff_type=None,
 
490
            delta_type=None, show_signature=None, omit_merges=None,
 
491
            generate_tags=None, specific_files=None, match=None,
 
492
            start_revision=None, end_revision=None, direction=None,
 
493
            exclude_common_ancestry=None, _match_using_deltas=None,
 
494
            signature=None):
431
495
        self.branch = branch
432
 
        self.rqst = rqst
433
 
        if rqst.get('generate_tags') and branch.supports_tags():
 
496
        self.levels = levels
 
497
        self.limit = limit
 
498
        self.diff_type = diff_type
 
499
        self.delta_type = delta_type
 
500
        self.show_signature = signature
 
501
        self.omit_merges = omit_merges
 
502
        self.specific_files = specific_files
 
503
        self.match = match
 
504
        self.start_revision = start_revision
 
505
        self.end_revision = end_revision
 
506
        self.direction = direction
 
507
        self.exclude_common_ancestry = exclude_common_ancestry
 
508
        self._match_using_deltas = _match_using_deltas
 
509
        if generate_tags and branch.supports_tags():
434
510
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
435
511
        else:
436
512
            self.rev_tag_dict = {}
440
516
 
441
517
        :return: An iterator yielding LogRevision objects.
442
518
        """
443
 
        rqst = self.rqst
444
 
        levels = rqst.get('levels')
445
 
        limit = rqst.get('limit')
446
 
        diff_type = rqst.get('diff_type')
447
 
        show_signature = rqst.get('signature')
448
 
        omit_merges = rqst.get('omit_merges')
449
519
        log_count = 0
450
520
        revision_iterator = self._create_log_revision_iterator()
451
521
        for revs in revision_iterator:
452
522
            for (rev_id, revno, merge_depth), rev, delta in revs:
453
523
                # 0 levels means show everything; merge_depth counts from 0
454
 
                if levels != 0 and merge_depth >= levels:
455
 
                    continue
456
 
                if omit_merges and len(rev.parent_ids) > 1:
457
 
                    continue
458
 
                if diff_type is None:
 
524
                if (self.levels != 0 and merge_depth is not None and
 
525
                        merge_depth >= self.levels):
 
526
                    continue
 
527
                if self.omit_merges and len(rev.parent_ids) > 1:
 
528
                    continue
 
529
                if rev is None:
 
530
                    raise errors.GhostRevisionUnusableHere(rev_id)
 
531
                if self.diff_type is None:
459
532
                    diff = None
460
533
                else:
461
 
                    diff = self._format_diff(rev, rev_id, diff_type)
462
 
                if show_signature:
463
 
                    signature = format_signature_validity(rev_id,
464
 
                                                self.branch.repository)
 
534
                    diff = _format_diff(
 
535
                        self.branch, rev, self.diff_type,
 
536
                        self.specific_files)
 
537
                if self.show_signature:
 
538
                    signature = format_signature_validity(rev_id, self.branch)
465
539
                else:
466
540
                    signature = None
467
 
                yield LogRevision(rev, revno, merge_depth, delta,
 
541
                yield LogRevision(
 
542
                    rev, revno, merge_depth, delta,
468
543
                    self.rev_tag_dict.get(rev_id), diff, signature)
469
 
                if limit:
 
544
                if self.limit:
470
545
                    log_count += 1
471
 
                    if log_count >= limit:
 
546
                    if log_count >= self.limit:
472
547
                        return
473
548
 
474
 
    def _format_diff(self, rev, rev_id, diff_type):
475
 
        repo = self.branch.repository
476
 
        if len(rev.parent_ids) == 0:
477
 
            ancestor_id = _mod_revision.NULL_REVISION
478
 
        else:
479
 
            ancestor_id = rev.parent_ids[0]
480
 
        tree_1 = repo.revision_tree(ancestor_id)
481
 
        tree_2 = repo.revision_tree(rev_id)
482
 
        file_ids = self.rqst.get('specific_fileids')
483
 
        if diff_type == 'partial' and file_ids is not None:
484
 
            specific_files = [tree_2.id2path(id) for id in file_ids]
485
 
        else:
486
 
            specific_files = None
487
 
        s = StringIO()
488
 
        path_encoding = get_diff_header_encoding()
489
 
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
490
 
            new_label='', path_encoding=path_encoding)
491
 
        return s.getvalue()
492
 
 
493
549
    def _create_log_revision_iterator(self):
494
550
        """Create a revision iterator for log.
495
551
 
496
552
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
497
553
            delta).
498
554
        """
499
 
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
500
 
            self.branch, self.rqst.get('start_revision'),
501
 
            self.rqst.get('end_revision'))
502
 
        if self.rqst.get('_match_using_deltas'):
503
 
            return self._log_revision_iterator_using_delta_matching()
 
555
        start_rev_id, end_rev_id = _get_revision_limits(
 
556
            self.branch, self.start_revision, self.end_revision)
 
557
        if self._match_using_deltas:
 
558
            return _log_revision_iterator_using_delta_matching(
 
559
                self.branch,
 
560
                delta_type=self.delta_type,
 
561
                match=self.match,
 
562
                levels=self.levels,
 
563
                specific_files=self.specific_files,
 
564
                start_rev_id=start_rev_id, end_rev_id=end_rev_id,
 
565
                direction=self.direction,
 
566
                exclude_common_ancestry=self.exclude_common_ancestry,
 
567
                limit=self.limit)
504
568
        else:
505
569
            # We're using the per-file-graph algorithm. This scales really
506
570
            # well but only makes sense if there is a single file and it's
507
571
            # not a directory
508
 
            file_count = len(self.rqst.get('specific_fileids'))
 
572
            file_count = len(self.specific_files)
509
573
            if file_count != 1:
510
 
                raise BzrError("illegal LogRequest: must match-using-deltas "
 
574
                raise errors.BzrError(
 
575
                    "illegal LogRequest: must match-using-deltas "
511
576
                    "when logging %d files" % file_count)
512
 
            return self._log_revision_iterator_using_per_file_graph()
513
 
 
514
 
    def _log_revision_iterator_using_delta_matching(self):
515
 
        # Get the base revisions, filtering by the revision range
516
 
        rqst = self.rqst
517
 
        generate_merge_revisions = rqst.get('levels') != 1
518
 
        delayed_graph_generation = not rqst.get('specific_fileids') and (
519
 
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
520
 
        view_revisions = _calc_view_revisions(
521
 
            self.branch, self.start_rev_id, self.end_rev_id,
522
 
            rqst.get('direction'),
523
 
            generate_merge_revisions=generate_merge_revisions,
524
 
            delayed_graph_generation=delayed_graph_generation,
525
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
526
 
 
527
 
        # Apply the other filters
528
 
        return make_log_rev_iterator(self.branch, view_revisions,
529
 
            rqst.get('delta_type'), rqst.get('match'),
530
 
            file_ids=rqst.get('specific_fileids'),
531
 
            direction=rqst.get('direction'))
532
 
 
533
 
    def _log_revision_iterator_using_per_file_graph(self):
534
 
        # Get the base revisions, filtering by the revision range.
535
 
        # Note that we always generate the merge revisions because
536
 
        # filter_revisions_touching_file_id() requires them ...
537
 
        rqst = self.rqst
538
 
        view_revisions = _calc_view_revisions(
539
 
            self.branch, self.start_rev_id, self.end_rev_id,
540
 
            rqst.get('direction'), generate_merge_revisions=True,
541
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
542
 
        if not isinstance(view_revisions, list):
543
 
            view_revisions = list(view_revisions)
544
 
        view_revisions = _filter_revisions_touching_file_id(self.branch,
545
 
            rqst.get('specific_fileids')[0], view_revisions,
546
 
            include_merges=rqst.get('levels') != 1)
547
 
        return make_log_rev_iterator(self.branch, view_revisions,
548
 
            rqst.get('delta_type'), rqst.get('match'))
 
577
            return _log_revision_iterator_using_per_file_graph(
 
578
                self.branch,
 
579
                delta_type=self.delta_type,
 
580
                match=self.match,
 
581
                levels=self.levels,
 
582
                path=self.specific_files[0],
 
583
                start_rev_id=start_rev_id, end_rev_id=end_rev_id,
 
584
                direction=self.direction,
 
585
                exclude_common_ancestry=self.exclude_common_ancestry
 
586
                )
549
587
 
550
588
 
551
589
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
559
597
             a list of the same tuples.
560
598
    """
561
599
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
562
 
        raise errors.BzrCommandError(gettext(
 
600
        raise errors.CommandError(gettext(
563
601
            '--exclude-common-ancestry requires two different revisions'))
564
602
    if direction not in ('reverse', 'forward'):
565
603
        raise ValueError(gettext('invalid direction %r') % direction)
566
 
    br_revno, br_rev_id = branch.last_revision_info()
567
 
    if br_revno == 0:
 
604
    br_rev_id = branch.last_revision()
 
605
    if br_rev_id == _mod_revision.NULL_REVISION:
568
606
        return []
569
607
 
570
608
    if (end_rev_id and start_rev_id == end_rev_id
571
609
        and (not generate_merge_revisions
572
610
             or not _has_merges(branch, end_rev_id))):
573
611
        # If a single revision is requested, check we can handle it
574
 
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
575
 
                                       br_revno)
 
612
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
 
613
                                      branch.revno())
576
614
    if not generate_merge_revisions:
577
615
        try:
578
616
            # If we only want to see linear revisions, we can iterate ...
583
621
            # ancestor of the end limit, check it before outputting anything
584
622
            if (direction == 'forward'
585
623
                or (start_rev_id and not _is_obvious_ancestor(
586
 
                        branch, start_rev_id, end_rev_id))):
587
 
                    iter_revs = list(iter_revs)
 
624
                    branch, start_rev_id, end_rev_id))):
 
625
                iter_revs = list(iter_revs)
588
626
            if direction == 'forward':
589
627
                iter_revs = reversed(iter_revs)
590
628
            return iter_revs
622
660
    initial_revisions = []
623
661
    if delayed_graph_generation:
624
662
        try:
625
 
            for rev_id, revno, depth in  _linear_view_revisions(
626
 
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
663
            for rev_id, revno, depth in _linear_view_revisions(
 
664
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
627
665
                if _has_merges(branch, rev_id):
628
666
                    # The end_rev_id can be nested down somewhere. We need an
629
667
                    # explicit ancestry check. There is an ambiguity here as we
636
674
                    # -- vila 20100319
637
675
                    graph = branch.repository.get_graph()
638
676
                    if (start_rev_id is not None
639
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
677
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
640
678
                        raise _StartNotLinearAncestor()
641
679
                    # Since we collected the revisions so far, we need to
642
680
                    # adjust end_rev_id.
650
688
        except _StartNotLinearAncestor:
651
689
            # A merge was never detected so the lower revision limit can't
652
690
            # be nested down somewhere
653
 
            raise errors.BzrCommandError(gettext('Start revision not found in'
654
 
                ' history of end revision.'))
 
691
            raise errors.CommandError(gettext('Start revision not found in'
 
692
                                                 ' history of end revision.'))
655
693
 
656
694
    # We exit the loop above because we encounter a revision with merges, from
657
695
    # this revision, we need to switch to _graph_view_revisions.
661
699
    # shown naturally, i.e. just like it is for linear logging. We can easily
662
700
    # make forward the exact opposite display, but showing the merge revisions
663
701
    # indented at the end seems slightly nicer in that case.
664
 
    view_revisions = chain(iter(initial_revisions),
665
 
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
666
 
                              rebase_initial_depths=(direction == 'reverse'),
667
 
                              exclude_common_ancestry=exclude_common_ancestry))
 
702
    view_revisions = itertools.chain(iter(initial_revisions),
 
703
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
704
                                                           rebase_initial_depths=(
 
705
                                                               direction == 'reverse'),
 
706
                                                           exclude_common_ancestry=exclude_common_ancestry))
668
707
    return view_revisions
669
708
 
670
709
 
702
741
            # both on mainline
703
742
            return start_dotted[0] <= end_dotted[0]
704
743
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
705
 
            start_dotted[0:1] == end_dotted[0:1]):
 
744
              start_dotted[0:1] == end_dotted[0:1]):
706
745
            # both on same development line
707
746
            return start_dotted[2] <= end_dotted[2]
708
747
        else:
722
761
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
723
762
        the iterated revisions.
724
763
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
764
        dotted_revno will be None for ghosts
725
765
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
726
766
        is not found walking the left-hand history
727
767
    """
728
 
    br_revno, br_rev_id = branch.last_revision_info()
729
768
    repo = branch.repository
730
769
    graph = repo.get_graph()
731
770
    if start_rev_id is None and end_rev_id is None:
732
 
        cur_revno = br_revno
733
 
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
734
 
            (_mod_revision.NULL_REVISION,)):
735
 
            yield revision_id, str(cur_revno), 0
736
 
            cur_revno -= 1
 
771
        if branch._format.stores_revno() or \
 
772
                config.GlobalStack().get('calculate_revnos'):
 
773
            try:
 
774
                br_revno, br_rev_id = branch.last_revision_info()
 
775
            except errors.GhostRevisionsHaveNoRevno:
 
776
                br_rev_id = branch.last_revision()
 
777
                cur_revno = None
 
778
            else:
 
779
                cur_revno = br_revno
 
780
        else:
 
781
            br_rev_id = branch.last_revision()
 
782
            cur_revno = None
 
783
 
 
784
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
 
785
                                                  (_mod_revision.NULL_REVISION,))
 
786
        while True:
 
787
            try:
 
788
                revision_id = next(graph_iter)
 
789
            except errors.RevisionNotPresent as e:
 
790
                # Oops, a ghost.
 
791
                yield e.revision_id, None, None
 
792
                break
 
793
            except StopIteration:
 
794
                break
 
795
            else:
 
796
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
 
797
                if cur_revno is not None:
 
798
                    cur_revno -= 1
737
799
    else:
 
800
        br_rev_id = branch.last_revision()
738
801
        if end_rev_id is None:
739
802
            end_rev_id = br_rev_id
740
803
        found_start = start_rev_id is None
741
 
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
742
 
                (_mod_revision.NULL_REVISION,)):
743
 
            revno_str = _compute_revno_str(branch, revision_id)
744
 
            if not found_start and revision_id == start_rev_id:
745
 
                if not exclude_common_ancestry:
 
804
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
 
805
                                                  (_mod_revision.NULL_REVISION,))
 
806
        while True:
 
807
            try:
 
808
                revision_id = next(graph_iter)
 
809
            except StopIteration:
 
810
                break
 
811
            except errors.RevisionNotPresent as e:
 
812
                # Oops, a ghost.
 
813
                yield e.revision_id, None, None
 
814
                break
 
815
            else:
 
816
                revno_str = _compute_revno_str(branch, revision_id)
 
817
                if not found_start and revision_id == start_rev_id:
 
818
                    if not exclude_common_ancestry:
 
819
                        yield revision_id, revno_str, 0
 
820
                    found_start = True
 
821
                    break
 
822
                else:
746
823
                    yield revision_id, revno_str, 0
747
 
                found_start = True
748
 
                break
749
 
            else:
750
 
                yield revision_id, revno_str, 0
751
 
        else:
752
 
            if not found_start:
753
 
                raise _StartNotLinearAncestor()
 
824
        if not found_start:
 
825
            raise _StartNotLinearAncestor()
754
826
 
755
827
 
756
828
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
801
873
    """Adjust depths upwards so the top level is 0."""
802
874
    # If either the first or last revision have a merge_depth of 0, we're done
803
875
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
804
 
        min_depth = min([d for r,n,d in view_revisions])
 
876
        min_depth = min([d for r, n, d in view_revisions])
805
877
        if min_depth != 0:
806
 
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
 
878
            view_revisions = [(r, n, d - min_depth)
 
879
                              for r, n, d in view_revisions]
807
880
    return view_revisions
808
881
 
809
882
 
810
883
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
811
 
        file_ids=None, direction='reverse'):
 
884
                          files=None, direction='reverse'):
812
885
    """Create a revision iterator for log.
813
886
 
814
887
    :param branch: The branch being logged.
816
889
    :param generate_delta: Whether to generate a delta for each revision.
817
890
      Permitted values are None, 'full' and 'partial'.
818
891
    :param search: A user text search string.
819
 
    :param file_ids: If non empty, only revisions matching one or more of
820
 
      the file-ids are to be kept.
 
892
    :param files: If non empty, only revisions matching one or more of
 
893
      the files are to be kept.
821
894
    :param direction: the direction in which view_revisions is sorted
822
895
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
823
896
        delta).
824
897
    """
825
898
    # Convert view_revisions into (view, None, None) groups to fit with
826
899
    # the standard interface here.
827
 
    if type(view_revisions) == list:
 
900
    if isinstance(view_revisions, list):
828
901
        # A single batch conversion is faster than many incremental ones.
829
902
        # As we have all the data, do a batch conversion.
830
903
        nones = [None] * len(view_revisions)
831
 
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
 
904
        log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
832
905
    else:
833
906
        def _convert():
834
907
            for view in view_revisions:
838
911
        # It would be nicer if log adapters were first class objects
839
912
        # with custom parameters. This will do for now. IGC 20090127
840
913
        if adapter == _make_delta_filter:
841
 
            log_rev_iterator = adapter(branch, generate_delta,
842
 
                search, log_rev_iterator, file_ids, direction)
 
914
            log_rev_iterator = adapter(
 
915
                branch, generate_delta, search, log_rev_iterator, files,
 
916
                direction)
843
917
        else:
844
 
            log_rev_iterator = adapter(branch, generate_delta,
845
 
                search, log_rev_iterator)
 
918
            log_rev_iterator = adapter(
 
919
                branch, generate_delta, search, log_rev_iterator)
846
920
    return log_rev_iterator
847
921
 
848
922
 
860
934
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
861
935
        delta).
862
936
    """
863
 
    if match is None:
 
937
    if not match:
864
938
        return log_rev_iterator
865
 
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
866
 
                for (k,v) in match.iteritems()]
 
939
    # Use lazy_compile so mapping to InvalidPattern error occurs.
 
940
    searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
 
941
                for k, v in match.items()]
867
942
    return _filter_re(searchRE, log_rev_iterator)
868
943
 
869
944
 
873
948
        if new_revs:
874
949
            yield new_revs
875
950
 
 
951
 
876
952
def _match_filter(searchRE, rev):
877
953
    strings = {
878
 
               'message': (rev.message,),
879
 
               'committer': (rev.committer,),
880
 
               'author': (rev.get_apparent_authors()),
881
 
               'bugs': list(rev.iter_bugs())
882
 
               }
883
 
    strings[''] = [item for inner_list in strings.itervalues()
 
954
        'message': (rev.message,),
 
955
        'committer': (rev.committer,),
 
956
        'author': (rev.get_apparent_authors()),
 
957
        'bugs': list(rev.iter_bugs())
 
958
        }
 
959
    strings[''] = [item for inner_list in strings.values()
884
960
                   for item in inner_list]
885
 
    for (k,v) in searchRE:
 
961
    for k, v in searchRE:
886
962
        if k in strings and not _match_any_filter(strings[k], v):
887
963
            return False
888
964
    return True
889
965
 
 
966
 
890
967
def _match_any_filter(strings, res):
891
 
    return any([filter(None, map(re.search, strings)) for re in res])
 
968
    return any(r.search(s) for r in res for s in strings)
 
969
 
892
970
 
893
971
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
894
 
    fileids=None, direction='reverse'):
 
972
                       files=None, direction='reverse'):
895
973
    """Add revision deltas to a log iterator if needed.
896
974
 
897
975
    :param branch: The branch being logged.
900
978
    :param search: A user text search string.
901
979
    :param log_rev_iterator: An input iterator containing all revisions that
902
980
        could be displayed, in lists.
903
 
    :param fileids: If non empty, only revisions matching one or more of
904
 
      the file-ids are to be kept.
 
981
    :param files: If non empty, only revisions matching one or more of
 
982
      the files are to be kept.
905
983
    :param direction: the direction in which view_revisions is sorted
906
984
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
907
985
        delta).
908
986
    """
909
 
    if not generate_delta and not fileids:
 
987
    if not generate_delta and not files:
910
988
        return log_rev_iterator
911
989
    return _generate_deltas(branch.repository, log_rev_iterator,
912
 
        generate_delta, fileids, direction)
913
 
 
914
 
 
915
 
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
916
 
    direction):
 
990
                            generate_delta, files, direction)
 
991
 
 
992
 
 
993
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
 
994
                     direction):
917
995
    """Create deltas for each batch of revisions in log_rev_iterator.
918
996
 
919
997
    If we're only generating deltas for the sake of filtering against
920
 
    file-ids, we stop generating deltas once all file-ids reach the
 
998
    files, we stop generating deltas once all files reach the
921
999
    appropriate life-cycle point. If we're receiving data newest to
922
1000
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
923
1001
    """
924
 
    check_fileids = fileids is not None and len(fileids) > 0
925
 
    if check_fileids:
926
 
        fileid_set = set(fileids)
 
1002
    check_files = files is not None and len(files) > 0
 
1003
    if check_files:
 
1004
        file_set = set(files)
927
1005
        if direction == 'reverse':
928
1006
            stop_on = 'add'
929
1007
        else:
930
1008
            stop_on = 'remove'
931
1009
    else:
932
 
        fileid_set = None
 
1010
        file_set = None
933
1011
    for revs in log_rev_iterator:
934
 
        # If we were matching against fileids and we've run out,
 
1012
        # If we were matching against files and we've run out,
935
1013
        # there's nothing left to do
936
 
        if check_fileids and not fileid_set:
 
1014
        if check_files and not file_set:
937
1015
            return
938
1016
        revisions = [rev[1] for rev in revs]
939
1017
        new_revs = []
940
 
        if delta_type == 'full' and not check_fileids:
941
 
            deltas = repository.get_deltas_for_revisions(revisions)
942
 
            for rev, delta in izip(revs, deltas):
 
1018
        if delta_type == 'full' and not check_files:
 
1019
            deltas = repository.get_revision_deltas(revisions)
 
1020
            for rev, delta in zip(revs, deltas):
943
1021
                new_revs.append((rev[0], rev[1], delta))
944
1022
        else:
945
 
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
946
 
            for rev, delta in izip(revs, deltas):
947
 
                if check_fileids:
 
1023
            deltas = repository.get_revision_deltas(
 
1024
                revisions, specific_files=file_set)
 
1025
            for rev, delta in zip(revs, deltas):
 
1026
                if check_files:
948
1027
                    if delta is None or not delta.has_changed():
949
1028
                        continue
950
1029
                    else:
951
 
                        _update_fileids(delta, fileid_set, stop_on)
 
1030
                        _update_files(delta, file_set, stop_on)
952
1031
                        if delta_type is None:
953
1032
                            delta = None
954
1033
                        elif delta_type == 'full':
965
1044
        yield new_revs
966
1045
 
967
1046
 
968
 
def _update_fileids(delta, fileids, stop_on):
969
 
    """Update the set of file-ids to search based on file lifecycle events.
 
1047
def _update_files(delta, files, stop_on):
 
1048
    """Update the set of files to search based on file lifecycle events.
970
1049
 
971
 
    :param fileids: a set of fileids to update
972
 
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
973
 
      fileids set once their add or remove entry is detected respectively
 
1050
    :param files: a set of files to update
 
1051
    :param stop_on: either 'add' or 'remove' - take files out of the
 
1052
      files set once their add or remove entry is detected respectively
974
1053
    """
975
1054
    if stop_on == 'add':
976
1055
        for item in delta.added:
977
 
            if item[1] in fileids:
978
 
                fileids.remove(item[1])
 
1056
            if item.path[1] in files:
 
1057
                files.remove(item.path[1])
 
1058
        for item in delta.copied + delta.renamed:
 
1059
            if item.path[1] in files:
 
1060
                files.remove(item.path[1])
 
1061
                files.add(item.path[0])
 
1062
            if item.kind[1] == 'directory':
 
1063
                for path in list(files):
 
1064
                    if is_inside(item.path[1], path):
 
1065
                        files.remove(path)
 
1066
                        files.add(item.path[0] + path[len(item.path[1]):])
979
1067
    elif stop_on == 'delete':
980
1068
        for item in delta.removed:
981
 
            if item[1] in fileids:
982
 
                fileids.remove(item[1])
 
1069
            if item.path[0] in files:
 
1070
                files.remove(item.path[0])
 
1071
        for item in delta.copied + delta.renamed:
 
1072
            if item.path[0] in files:
 
1073
                files.remove(item.path[0])
 
1074
                files.add(item.path[1])
 
1075
            if item.kind[0] == 'directory':
 
1076
                for path in list(files):
 
1077
                    if is_inside(item.path[0], path):
 
1078
                        files.remove(path)
 
1079
                        files.add(item.path[1] + path[len(item.path[0]):])
983
1080
 
984
1081
 
985
1082
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
997
1094
    for revs in log_rev_iterator:
998
1095
        # r = revision_id, n = revno, d = merge depth
999
1096
        revision_ids = [view[0] for view, _, _ in revs]
1000
 
        revisions = repository.get_revisions(revision_ids)
1001
 
        revs = [(rev[0], revision, rev[2]) for rev, revision in
1002
 
            izip(revs, revisions)]
1003
 
        yield revs
 
1097
        revisions = dict(repository.iter_revisions(revision_ids))
 
1098
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
1004
1099
 
1005
1100
 
1006
1101
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
1028
1123
def _get_revision_limits(branch, start_revision, end_revision):
1029
1124
    """Get and check revision limits.
1030
1125
 
1031
 
    :param  branch: The branch containing the revisions.
1032
 
 
1033
 
    :param  start_revision: The first revision to be logged.
1034
 
            For backwards compatibility this may be a mainline integer revno,
1035
 
            but for merge revision support a RevisionInfo is expected.
1036
 
 
1037
 
    :param  end_revision: The last revision to be logged.
1038
 
            For backwards compatibility this may be a mainline integer revno,
1039
 
            but for merge revision support a RevisionInfo is expected.
 
1126
    :param branch: The branch containing the revisions.
 
1127
 
 
1128
    :param start_revision: The first revision to be logged, as a RevisionInfo.
 
1129
 
 
1130
    :param end_revision: The last revision to be logged, as a RevisionInfo
1040
1131
 
1041
1132
    :return: (start_rev_id, end_rev_id) tuple.
1042
1133
    """
1043
 
    branch_revno, branch_rev_id = branch.last_revision_info()
1044
1134
    start_rev_id = None
1045
 
    if start_revision is None:
 
1135
    start_revno = None
 
1136
    if start_revision is not None:
 
1137
        if not isinstance(start_revision, revisionspec.RevisionInfo):
 
1138
            raise TypeError(start_revision)
 
1139
        start_rev_id = start_revision.rev_id
 
1140
        start_revno = start_revision.revno
 
1141
    if start_revno is None:
1046
1142
        start_revno = 1
1047
 
    else:
1048
 
        if isinstance(start_revision, revisionspec.RevisionInfo):
1049
 
            start_rev_id = start_revision.rev_id
1050
 
            start_revno = start_revision.revno or 1
1051
 
        else:
1052
 
            branch.check_real_revno(start_revision)
1053
 
            start_revno = start_revision
1054
 
            start_rev_id = branch.get_rev_id(start_revno)
1055
1143
 
1056
1144
    end_rev_id = None
1057
 
    if end_revision is None:
1058
 
        end_revno = branch_revno
1059
 
    else:
1060
 
        if isinstance(end_revision, revisionspec.RevisionInfo):
1061
 
            end_rev_id = end_revision.rev_id
1062
 
            end_revno = end_revision.revno or branch_revno
1063
 
        else:
1064
 
            branch.check_real_revno(end_revision)
1065
 
            end_revno = end_revision
1066
 
            end_rev_id = branch.get_rev_id(end_revno)
 
1145
    end_revno = None
 
1146
    if end_revision is not None:
 
1147
        if not isinstance(end_revision, revisionspec.RevisionInfo):
 
1148
            raise TypeError(start_revision)
 
1149
        end_rev_id = end_revision.rev_id
 
1150
        end_revno = end_revision.revno
1067
1151
 
1068
 
    if branch_revno != 0:
 
1152
    if branch.last_revision() != _mod_revision.NULL_REVISION:
1069
1153
        if (start_rev_id == _mod_revision.NULL_REVISION
1070
 
            or end_rev_id == _mod_revision.NULL_REVISION):
1071
 
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1072
 
        if start_revno > end_revno:
1073
 
            raise errors.BzrCommandError(gettext("Start revision must be "
1074
 
                                         "older than the end revision."))
 
1154
                or end_rev_id == _mod_revision.NULL_REVISION):
 
1155
            raise errors.CommandError(
 
1156
                gettext('Logging revision 0 is invalid.'))
 
1157
        if end_revno is not None and start_revno > end_revno:
 
1158
            raise errors.CommandError(
 
1159
                gettext("Start revision must be older than the end revision."))
1075
1160
    return (start_rev_id, end_rev_id)
1076
1161
 
1077
1162
 
1125
1210
            end_revno = end_revision
1126
1211
 
1127
1212
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1128
 
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1129
 
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1213
            or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1214
        raise errors.CommandError(gettext('Logging revision 0 is invalid.'))
1130
1215
    if start_revno > end_revno:
1131
 
        raise errors.BzrCommandError(gettext("Start revision must be older "
1132
 
                                     "than the end revision."))
 
1216
        raise errors.CommandError(gettext("Start revision must be older "
 
1217
                                             "than the end revision."))
1133
1218
 
1134
1219
    if end_revno < start_revno:
1135
1220
        return None, None, None, None
1158
1243
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1159
1244
 
1160
1245
 
1161
 
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1162
 
    include_merges=True):
1163
 
    r"""Return the list of revision ids which touch a given file id.
 
1246
def _filter_revisions_touching_path(branch, path, view_revisions,
 
1247
                                    include_merges=True):
 
1248
    r"""Return the list of revision ids which touch a given path.
1164
1249
 
1165
1250
    The function filters view_revisions and returns a subset.
1166
 
    This includes the revisions which directly change the file id,
 
1251
    This includes the revisions which directly change the path,
1167
1252
    and the revisions which merge these changes. So if the
1168
1253
    revision graph is::
1169
1254
 
1186
1271
 
1187
1272
    :param branch: The branch where we can get text revision information.
1188
1273
 
1189
 
    :param file_id: Filter out revisions that do not touch file_id.
 
1274
    :param path: Filter out revisions that do not touch path.
1190
1275
 
1191
1276
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1192
1277
        tuples. This is the list of revisions which will be filtered. It is
1200
1285
    # Lookup all possible text keys to determine which ones actually modified
1201
1286
    # the file.
1202
1287
    graph = branch.repository.get_file_graph()
 
1288
    start_tree = branch.repository.revision_tree(view_revisions[0][0])
 
1289
    file_id = start_tree.path2id(path)
1203
1290
    get_parent_map = graph.get_parent_map
1204
1291
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1205
1292
    next_keys = None
1212
1299
    #       rate). This particular access is clustered with a low success rate.
1213
1300
    modified_text_revisions = set()
1214
1301
    chunk_size = 1000
1215
 
    for start in xrange(0, len(text_keys), chunk_size):
 
1302
    for start in range(0, len(text_keys), chunk_size):
1216
1303
        next_keys = text_keys[start:start + chunk_size]
1217
1304
        # Only keep the revision_id portion of the key
1218
1305
        modified_text_revisions.update(
1233
1320
 
1234
1321
        if rev_id in modified_text_revisions:
1235
1322
            # This needs to be logged, along with the extra revisions
1236
 
            for idx in xrange(len(current_merge_stack)):
 
1323
            for idx in range(len(current_merge_stack)):
1237
1324
                node = current_merge_stack[idx]
1238
1325
                if node is not None:
1239
1326
                    if include_merges or node[2] == 0:
1246
1333
    """Reverse revisions by depth.
1247
1334
 
1248
1335
    Revisions with a different depth are sorted as a group with the previous
1249
 
    revision of that depth.  There may be no topological justification for this,
 
1336
    revision of that depth.  There may be no topological justification for this
1250
1337
    but it looks much nicer.
1251
1338
    """
1252
1339
    # Add a fake revision at start so that we can always attach sub revisions
1359
1446
        """
1360
1447
        self.to_file = to_file
1361
1448
        # 'exact' stream used to show diff, it should print content 'as is'
1362
 
        # and should not try to decode/encode it to unicode to avoid bug #328007
 
1449
        # and should not try to decode/encode it to unicode to avoid bug
 
1450
        # #328007
1363
1451
        if to_exact_file is not None:
1364
1452
            self.to_exact_file = to_exact_file
1365
1453
        else:
1366
 
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
1367
 
            # for code that expects to get diffs to pass in the exact file
1368
 
            # stream
 
1454
            # XXX: somewhat hacky; this assumes it's a codec writer; it's
 
1455
            # better for code that expects to get diffs to pass in the exact
 
1456
            # file stream
1369
1457
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1370
1458
        self.show_ids = show_ids
1371
1459
        self.show_timezone = show_timezone
1372
1460
        if delta_format is None:
1373
1461
            # Ensures backward compatibility
1374
 
            delta_format = 2 # long format
 
1462
            delta_format = 2  # long format
1375
1463
        self.delta_format = delta_format
1376
1464
        self.levels = levels
1377
1465
        self._show_advice = show_advice
1475
1563
        """
1476
1564
        lines = self._foreign_info_properties(revision)
1477
1565
        for key, handler in properties_handler_registry.iteritems():
1478
 
            lines.extend(self._format_properties(handler(revision)))
 
1566
            try:
 
1567
                lines.extend(self._format_properties(handler(revision)))
 
1568
            except Exception:
 
1569
                trace.log_exception_quietly()
 
1570
                trace.print_exception(sys.exc_info(), self.to_file)
1479
1571
        return lines
1480
1572
 
1481
1573
    def _foreign_info_properties(self, rev):
1489
1581
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1490
1582
 
1491
1583
        # Imported foreign revision revision ids always contain :
1492
 
        if not ":" in rev.revision_id:
 
1584
        if b":" not in rev.revision_id:
1493
1585
            return []
1494
1586
 
1495
1587
        # Revision was once imported from a foreign repository
1509
1601
        return lines
1510
1602
 
1511
1603
    def show_diff(self, to_file, diff, indent):
1512
 
        for l in diff.rstrip().split('\n'):
1513
 
            to_file.write(indent + '%s\n' % (l,))
 
1604
        encoding = get_terminal_encoding()
 
1605
        for l in diff.rstrip().split(b'\n'):
 
1606
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
1514
1607
 
1515
1608
 
1516
1609
# Separator between revisions in long format
1539
1632
 
1540
1633
    def _date_string_original_timezone(self, rev):
1541
1634
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1542
 
            rev.timezone or 0)
 
1635
                                                            rev.timezone or 0)
1543
1636
 
1544
1637
    def log_revision(self, revision):
1545
1638
        """Log a revision, either merged or not."""
1547
1640
        lines = [_LONG_SEP]
1548
1641
        if revision.revno is not None:
1549
1642
            lines.append('revno: %s%s' % (revision.revno,
1550
 
                self.merge_marker(revision)))
 
1643
                                          self.merge_marker(revision)))
1551
1644
        if revision.tags:
1552
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1645
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
1553
1646
        if self.show_ids or revision.revno is None:
1554
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1647
            lines.append('revision-id: %s' %
 
1648
                         (revision.rev.revision_id.decode('utf-8'),))
1555
1649
        if self.show_ids:
1556
1650
            for parent_id in revision.rev.parent_ids:
1557
 
                lines.append('parent: %s' % (parent_id,))
 
1651
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1558
1652
        lines.extend(self.custom_properties(revision.rev))
1559
1653
 
1560
1654
        committer = revision.rev.committer
1585
1679
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1586
1680
        if revision.delta is not None:
1587
1681
            # Use the standard status output to display changes
1588
 
            from bzrlib.delta import report_delta
 
1682
            from breezy.delta import report_delta
1589
1683
            report_delta(to_file, revision.delta, short_status=False,
1590
1684
                         show_ids=self.show_ids, indent=indent)
1591
1685
        if revision.diff is not None:
1636
1730
        to_file = self.to_file
1637
1731
        tags = ''
1638
1732
        if revision.tags:
1639
 
            tags = ' {%s}' % (', '.join(revision.tags))
 
1733
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1640
1734
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1641
 
                revision.revno or "", self.short_author(revision.rev),
1642
 
                format_date(revision.rev.timestamp,
1643
 
                            revision.rev.timezone or 0,
1644
 
                            self.show_timezone, date_fmt="%Y-%m-%d",
1645
 
                            show_offset=False),
1646
 
                tags, self.merge_marker(revision)))
1647
 
        self.show_properties(revision.rev, indent+offset)
 
1735
                                                     revision.revno or "", self.short_author(
 
1736
                                                         revision.rev),
 
1737
                                                     format_date(revision.rev.timestamp,
 
1738
                                                                 revision.rev.timezone or 0,
 
1739
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
 
1740
                                                                 show_offset=False),
 
1741
                                                     tags, self.merge_marker(revision)))
 
1742
        self.show_properties(revision.rev, indent + offset)
1648
1743
        if self.show_ids or revision.revno is None:
1649
1744
            to_file.write(indent + offset + 'revision-id:%s\n'
1650
 
                          % (revision.rev.revision_id,))
 
1745
                          % (revision.rev.revision_id.decode('utf-8'),))
1651
1746
        if not revision.rev.message:
1652
1747
            to_file.write(indent + offset + '(no message)\n')
1653
1748
        else:
1657
1752
 
1658
1753
        if revision.delta is not None:
1659
1754
            # Use the standard status output to display changes
1660
 
            from bzrlib.delta import report_delta
 
1755
            from breezy.delta import report_delta
1661
1756
            report_delta(to_file, revision.delta,
1662
 
                         short_status=self.delta_format==1,
 
1757
                         short_status=self.delta_format == 1,
1663
1758
                         show_ids=self.show_ids, indent=indent + offset)
1664
1759
        if revision.diff is not None:
1665
1760
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1683
1778
    def truncate(self, str, max_len):
1684
1779
        if max_len is None or len(str) <= max_len:
1685
1780
            return str
1686
 
        return str[:max_len-3] + '...'
 
1781
        return str[:max_len - 3] + '...'
1687
1782
 
1688
1783
    def date_string(self, rev):
1689
1784
        return format_date(rev.timestamp, rev.timezone or 0,
1699
1794
    def log_revision(self, revision):
1700
1795
        indent = '  ' * revision.merge_depth
1701
1796
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1702
 
            self._max_chars, revision.tags, indent))
 
1797
                                           self._max_chars, revision.tags, indent))
1703
1798
        self.to_file.write('\n')
1704
1799
 
1705
1800
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1718
1813
            # show revno only when is not None
1719
1814
            out.append("%s:" % revno)
1720
1815
        if max_chars is not None:
1721
 
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1816
            out.append(self.truncate(
 
1817
                self.short_author(rev), (max_chars + 3) // 4))
1722
1818
        else:
1723
1819
            out.append(self.short_author(rev))
1724
1820
        out.append(self.date_string(rev))
1725
1821
        if len(rev.parent_ids) > 1:
1726
1822
            out.append('[merge]')
1727
1823
        if tags:
1728
 
            tag_str = '{%s}' % (', '.join(tags))
 
1824
            tag_str = '{%s}' % (', '.join(sorted(tags)))
1729
1825
            out.append(tag_str)
1730
1826
        out.append(rev.get_summary())
1731
1827
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1747
1843
                               show_offset=False)
1748
1844
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1749
1845
        committer_str = committer_str.replace(' <', '  <')
1750
 
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
 
1846
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
1751
1847
 
1752
1848
        if revision.delta is not None and revision.delta.has_changed():
1753
1849
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1754
 
                path, = c[:1]
 
1850
                if c.path[0] is None:
 
1851
                    path = c.path[1]
 
1852
                else:
 
1853
                    path = c.path[0]
1755
1854
                to_file.write('\t* %s:\n' % (path,))
1756
 
            for c in revision.delta.renamed:
1757
 
                oldpath,newpath = c[:2]
 
1855
            for c in revision.delta.renamed + revision.delta.copied:
1758
1856
                # For renamed files, show both the old and the new path
1759
 
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
 
1857
                to_file.write('\t* %s:\n\t* %s:\n' % (c.path[0], c.path[1]))
1760
1858
            to_file.write('\n')
1761
1859
 
1762
1860
        if not revision.rev.message:
1815
1913
    try:
1816
1914
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1817
1915
    except KeyError:
1818
 
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1916
        raise errors.CommandError(
 
1917
            gettext("unknown log formatter: %r") % name)
1819
1918
 
1820
1919
 
1821
1920
def author_list_all(rev):
1857
1956
    """
1858
1957
    if to_file is None:
1859
1958
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1860
 
            errors='replace')
 
1959
                                                            errors='replace')
1861
1960
    lf = log_formatter(log_format,
1862
1961
                       show_ids=False,
1863
1962
                       to_file=to_file,
1866
1965
    # This is the first index which is different between
1867
1966
    # old and new
1868
1967
    base_idx = None
1869
 
    for i in xrange(max(len(new_rh),
1870
 
                        len(old_rh))):
 
1968
    for i in range(max(len(new_rh), len(old_rh))):
1871
1969
        if (len(new_rh) <= i
1872
1970
            or len(old_rh) <= i
1873
 
            or new_rh[i] != old_rh[i]):
 
1971
                or new_rh[i] != old_rh[i]):
1874
1972
            base_idx = i
1875
1973
            break
1876
1974
 
1877
1975
    if base_idx is None:
1878
1976
        to_file.write('Nothing seems to have changed\n')
1879
1977
        return
1880
 
    ## TODO: It might be nice to do something like show_log
1881
 
    ##       and show the merged entries. But since this is the
1882
 
    ##       removed revisions, it shouldn't be as important
 
1978
    # TODO: It might be nice to do something like show_log
 
1979
    # and show the merged entries. But since this is the
 
1980
    # removed revisions, it shouldn't be as important
1883
1981
    if base_idx < len(old_rh):
1884
 
        to_file.write('*'*60)
 
1982
        to_file.write('*' * 60)
1885
1983
        to_file.write('\nRemoved Revisions:\n')
1886
1984
        for i in range(base_idx, len(old_rh)):
1887
1985
            rev = branch.repository.get_revision(old_rh[i])
1888
 
            lr = LogRevision(rev, i+1, 0, None)
 
1986
            lr = LogRevision(rev, i + 1, 0, None)
1889
1987
            lf.log_revision(lr)
1890
 
        to_file.write('*'*60)
 
1988
        to_file.write('*' * 60)
1891
1989
        to_file.write('\n\n')
1892
1990
    if base_idx < len(new_rh):
1893
1991
        to_file.write('Added Revisions:\n')
1894
1992
        show_log(branch,
1895
1993
                 lf,
1896
 
                 None,
1897
1994
                 verbose=False,
1898
1995
                 direction='forward',
1899
 
                 start_revision=base_idx+1,
1900
 
                 end_revision=len(new_rh),
1901
 
                 search=None)
 
1996
                 start_revision=base_idx + 1,
 
1997
                 end_revision=len(new_rh))
1902
1998
 
1903
1999
 
1904
2000
def get_history_change(old_revision_id, new_revision_id, repository):
1923
2019
    while do_new or do_old:
1924
2020
        if do_new:
1925
2021
            try:
1926
 
                new_revision = new_iter.next()
 
2022
                new_revision = next(new_iter)
1927
2023
            except StopIteration:
1928
2024
                do_new = False
1929
2025
            else:
1934
2030
                    break
1935
2031
        if do_old:
1936
2032
            try:
1937
 
                old_revision = old_iter.next()
 
2033
                old_revision = next(old_iter)
1938
2034
            except StopIteration:
1939
2035
                do_old = False
1940
2036
            else:
1970
2066
    log_format = log_formatter_registry.get_default(branch)
1971
2067
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
1972
2068
    if old_history != []:
1973
 
        output.write('*'*60)
 
2069
        output.write('*' * 60)
1974
2070
        output.write('\nRemoved Revisions:\n')
1975
2071
        show_flat_log(branch.repository, old_history, old_revno, lf)
1976
 
        output.write('*'*60)
 
2072
        output.write('*' * 60)
1977
2073
        output.write('\n\n')
1978
2074
    if new_history != []:
1979
2075
        output.write('Added Revisions:\n')
1980
2076
        start_revno = new_revno - len(new_history) + 1
1981
 
        show_log(branch, lf, None, verbose=False, direction='forward',
1982
 
                 start_revision=start_revno,)
 
2077
        show_log(branch, lf, verbose=False, direction='forward',
 
2078
                 start_revision=start_revno)
1983
2079
 
1984
2080
 
1985
2081
def show_flat_log(repository, history, last_revno, lf):
1990
2086
    :param last_revno: The revno of the last revision_id in the history.
1991
2087
    :param lf: The log formatter to use.
1992
2088
    """
1993
 
    start_revno = last_revno - len(history) + 1
1994
2089
    revisions = repository.get_revisions(history)
1995
2090
    for i, rev in enumerate(revisions):
1996
2091
        lr = LogRevision(rev, i + last_revno, 0, None)
1997
2092
        lf.log_revision(lr)
1998
2093
 
1999
2094
 
2000
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
2001
 
    """Find file-ids and kinds given a list of files and a revision range.
 
2095
def _get_info_for_log_files(revisionspec_list, file_list, exit_stack):
 
2096
    """Find files and kinds given a list of files and a revision range.
2002
2097
 
2003
2098
    We search for files at the end of the range. If not found there,
2004
2099
    we try the start of the range.
2007
2102
    :param file_list: the list of paths given on the command line;
2008
2103
      the first of these can be a branch location or a file path,
2009
2104
      the remainder must be file paths
2010
 
    :param add_cleanup: When the branch returned is read locked,
2011
 
      an unlock call will be queued to the cleanup.
 
2105
    :param exit_stack: When the branch returned is read locked,
 
2106
      an unlock call will be queued to the exit stack.
2012
2107
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2013
 
      info_list is a list of (relative_path, file_id, kind) tuples where
 
2108
      info_list is a list of (relative_path, found, kind) tuples where
2014
2109
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2015
2110
      branch will be read-locked.
2016
2111
    """
2017
 
    from bzrlib.builtins import _get_revision_range
 
2112
    from breezy.builtins import _get_revision_range
2018
2113
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2019
2114
        file_list[0])
2020
 
    add_cleanup(b.lock_read().unlock)
 
2115
    exit_stack.enter_context(b.lock_read())
2021
2116
    # XXX: It's damn messy converting a list of paths to relative paths when
2022
2117
    # those paths might be deleted ones, they might be on a case-insensitive
2023
2118
    # filesystem and/or they might be in silly locations (like another branch).
2032
2127
        relpaths = [path] + file_list[1:]
2033
2128
    info_list = []
2034
2129
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2035
 
        "log")
 
2130
                                                       "log")
2036
2131
    if relpaths in ([], [u'']):
2037
2132
        return b, [], start_rev_info, end_rev_info
2038
2133
    if start_rev_info is None and end_rev_info is None:
2040
2135
            tree = b.basis_tree()
2041
2136
        tree1 = None
2042
2137
        for fp in relpaths:
2043
 
            file_id = tree.path2id(fp)
2044
 
            kind = _get_kind_for_file_id(tree, file_id)
2045
 
            if file_id is None:
 
2138
            kind = _get_kind_for_file(tree, fp)
 
2139
            if not kind:
2046
2140
                # go back to when time began
2047
2141
                if tree1 is None:
2048
2142
                    try:
2049
2143
                        rev1 = b.get_rev_id(1)
2050
2144
                    except errors.NoSuchRevision:
2051
2145
                        # No history at all
2052
 
                        file_id = None
2053
2146
                        kind = None
2054
2147
                    else:
2055
2148
                        tree1 = b.repository.revision_tree(rev1)
2056
2149
                if tree1:
2057
 
                    file_id = tree1.path2id(fp)
2058
 
                    kind = _get_kind_for_file_id(tree1, file_id)
2059
 
            info_list.append((fp, file_id, kind))
 
2150
                    kind = _get_kind_for_file(tree1, fp)
 
2151
            info_list.append((fp, kind))
2060
2152
 
2061
2153
    elif start_rev_info == end_rev_info:
2062
2154
        # One revision given - file must exist in it
2063
2155
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2064
2156
        for fp in relpaths:
2065
 
            file_id = tree.path2id(fp)
2066
 
            kind = _get_kind_for_file_id(tree, file_id)
2067
 
            info_list.append((fp, file_id, kind))
 
2157
            kind = _get_kind_for_file(tree, fp)
 
2158
            info_list.append((fp, kind))
2068
2159
 
2069
2160
    else:
2070
2161
        # Revision range given. Get the file-id from the end tree.
2076
2167
            tree = b.repository.revision_tree(rev_id)
2077
2168
        tree1 = None
2078
2169
        for fp in relpaths:
2079
 
            file_id = tree.path2id(fp)
2080
 
            kind = _get_kind_for_file_id(tree, file_id)
2081
 
            if file_id is None:
 
2170
            kind = _get_kind_for_file(tree, fp)
 
2171
            if not kind:
2082
2172
                if tree1 is None:
2083
2173
                    rev_id = start_rev_info.rev_id
2084
2174
                    if rev_id is None:
2086
2176
                        tree1 = b.repository.revision_tree(rev1)
2087
2177
                    else:
2088
2178
                        tree1 = b.repository.revision_tree(rev_id)
2089
 
                file_id = tree1.path2id(fp)
2090
 
                kind = _get_kind_for_file_id(tree1, file_id)
2091
 
            info_list.append((fp, file_id, kind))
 
2179
                kind = _get_kind_for_file(tree1, fp)
 
2180
            info_list.append((fp, kind))
2092
2181
    return b, info_list, start_rev_info, end_rev_info
2093
2182
 
2094
2183
 
2095
 
def _get_kind_for_file_id(tree, file_id):
2096
 
    """Return the kind of a file-id or None if it doesn't exist."""
2097
 
    if file_id is not None:
2098
 
        return tree.kind(file_id)
2099
 
    else:
2100
 
        return None
 
2184
def _get_kind_for_file(tree, path):
 
2185
    """Return the kind of a path or None if it doesn't exist."""
 
2186
    with tree.lock_read():
 
2187
        try:
 
2188
            return tree.stored_kind(path)
 
2189
        except errors.NoSuchFile:
 
2190
            return None
2101
2191
 
2102
2192
 
2103
2193
properties_handler_registry = registry.Registry()
2104
2194
 
2105
2195
# Use the properties handlers to print out bug information if available
 
2196
 
 
2197
 
2106
2198
def _bugs_properties_handler(revision):
2107
 
    if revision.properties.has_key('bugs'):
2108
 
        bug_lines = revision.properties['bugs'].split('\n')
2109
 
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2110
 
        fixed_bug_urls = [row[0] for row in bug_rows if
2111
 
                          len(row) > 1 and row[1] == 'fixed']
 
2199
    fixed_bug_urls = []
 
2200
    related_bug_urls = []
 
2201
    for bug_url, status in revision.iter_bugs():
 
2202
        if status == 'fixed':
 
2203
            fixed_bug_urls.append(bug_url)
 
2204
        elif status == 'related':
 
2205
            related_bug_urls.append(bug_url)
 
2206
    ret = {}
 
2207
    if fixed_bug_urls:
 
2208
        text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
 
2209
        ret[text] = ' '.join(fixed_bug_urls)
 
2210
    if related_bug_urls:
 
2211
        text = ngettext('related bug', 'related bugs',
 
2212
                        len(related_bug_urls))
 
2213
        ret[text] = ' '.join(related_bug_urls)
 
2214
    return ret
2112
2215
 
2113
 
        if fixed_bug_urls:
2114
 
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
2115
 
                    ' '.join(fixed_bug_urls)}
2116
 
    return {}
2117
2216
 
2118
2217
properties_handler_registry.register('bugs_properties_handler',
2119
2218
                                     _bugs_properties_handler)