~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jelmer Vernooij
  • Date: 2011-12-15 11:53:48 UTC
  • mto: This revision was merged to the branch mainline in revision 6375.
  • Revision ID: jelmer@samba.org-20111215115348-murs91ipn8jbw6y0
Add tests for default_email behaviour.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
65
65
lazy_import(globals(), """
66
66
 
67
67
from bzrlib import (
68
 
    bzrdir,
69
68
    config,
 
69
    controldir,
70
70
    diff,
71
71
    errors,
72
72
    foreign,
73
73
    repository as _mod_repository,
74
74
    revision as _mod_revision,
75
75
    revisionspec,
76
 
    trace,
77
76
    tsort,
78
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
79
79
""")
80
80
 
81
81
from bzrlib import (
 
82
    lazy_regex,
82
83
    registry,
83
84
    )
84
85
from bzrlib.osutils import (
85
86
    format_date,
86
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
87
89
    get_terminal_encoding,
88
 
    re_compile_checked,
89
90
    terminal_width,
90
91
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
94
 
    )
95
92
 
96
93
 
97
94
def find_touching_revisions(branch, file_id):
108
105
    last_ie = None
109
106
    last_path = None
110
107
    revno = 1
111
 
    for revision_id in branch.revision_history():
 
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
112
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if file_id in this_inv:
 
113
        if this_inv.has_id(file_id):
114
114
            this_ie = this_inv[file_id]
115
115
            this_path = this_inv.id2path(file_id)
116
116
        else:
156
156
             end_revision=None,
157
157
             search=None,
158
158
             limit=None,
159
 
             show_diff=False):
 
159
             show_diff=False,
 
160
             match=None):
160
161
    """Write out human-readable log of commits to this branch.
161
162
 
162
163
    This function is being retained for backwards compatibility but
185
186
        if None or 0.
186
187
 
187
188
    :param show_diff: If True, output a diff after each revision.
 
189
 
 
190
    :param match: Dictionary of search lists to use when matching revision
 
191
      properties.
188
192
    """
189
193
    # Convert old-style parameters to new-style parameters
190
194
    if specific_fileid is not None:
214
218
    Logger(branch, rqst).show(lf)
215
219
 
216
220
 
217
 
# Note: This needs to be kept this in sync with the defaults in
 
221
# Note: This needs to be kept in sync with the defaults in
218
222
# make_log_request_dict() below
219
223
_DEFAULT_REQUEST_PARAMS = {
220
224
    'direction': 'reverse',
221
 
    'levels': 1,
 
225
    'levels': None,
222
226
    'generate_tags': True,
223
227
    'exclude_common_ancestry': False,
224
228
    '_match_using_deltas': True,
227
231
 
228
232
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
233
                          start_revision=None, end_revision=None, limit=None,
230
 
                          message_search=None, levels=1, generate_tags=True,
 
234
                          message_search=None, levels=None, generate_tags=True,
231
235
                          delta_type=None,
232
236
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
 
237
                          exclude_common_ancestry=False, match=None,
 
238
                          signature=False, omit_merges=False,
234
239
                          ):
235
240
    """Convenience function for making a logging request dictionary.
236
241
 
257
262
      matching commit messages
258
263
 
259
264
    :param levels: the number of levels of revisions to
260
 
      generate; 1 for just the mainline; 0 for all levels.
 
265
      generate; 1 for just the mainline; 0 for all levels, or None for
 
266
      a sensible default.
261
267
 
262
268
    :param generate_tags: If True, include tags for matched revisions.
263
 
 
 
269
`
264
270
    :param delta_type: Either 'full', 'partial' or None.
265
271
      'full' means generate the complete delta - adds/deletes/modifies/etc;
266
272
      'partial' means filter the delta using specific_fileids;
278
284
 
279
285
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
286
      range operator or as a graph difference.
 
287
 
 
288
    :param signature: show digital signature information
 
289
 
 
290
    :param match: Dictionary of list of search strings to use when filtering
 
291
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
292
      the empty string to match any of the preceding properties.
 
293
 
 
294
    :param omit_merges: If True, commits with more than one parent are
 
295
      omitted.
 
296
 
281
297
    """
 
298
    # Take care of old style message_search parameter
 
299
    if message_search:
 
300
        if match:
 
301
            if 'message' in match:
 
302
                match['message'].append(message_search)
 
303
            else:
 
304
                match['message'] = [message_search]
 
305
        else:
 
306
            match={ 'message': [message_search] }
282
307
    return {
283
308
        'direction': direction,
284
309
        'specific_fileids': specific_fileids,
285
310
        'start_revision': start_revision,
286
311
        'end_revision': end_revision,
287
312
        'limit': limit,
288
 
        'message_search': message_search,
289
313
        'levels': levels,
290
314
        'generate_tags': generate_tags,
291
315
        'delta_type': delta_type,
292
316
        'diff_type': diff_type,
293
317
        'exclude_common_ancestry': exclude_common_ancestry,
 
318
        'signature': signature,
 
319
        'match': match,
 
320
        'omit_merges': omit_merges,
294
321
        # Add 'private' attributes for features that may be deprecated
295
322
        '_match_using_deltas': _match_using_deltas,
296
323
    }
298
325
 
299
326
def _apply_log_request_defaults(rqst):
300
327
    """Apply default values to a request dictionary."""
301
 
    result = _DEFAULT_REQUEST_PARAMS
 
328
    result = _DEFAULT_REQUEST_PARAMS.copy()
302
329
    if rqst:
303
330
        result.update(rqst)
304
331
    return result
305
332
 
306
333
 
 
334
def format_signature_validity(rev_id, repo):
 
335
    """get the signature validity
 
336
 
 
337
    :param rev_id: revision id to validate
 
338
    :param repo: repository of revision
 
339
    :return: human readable string to print to log
 
340
    """
 
341
    from bzrlib import gpg
 
342
 
 
343
    gpg_strategy = gpg.GPGStrategy(None)
 
344
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
 
345
    if result[0] == gpg.SIGNATURE_VALID:
 
346
        return "valid signature from {0}".format(result[1])
 
347
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
348
        return "unknown key {0}".format(result[1])
 
349
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
350
        return "invalid signature!"
 
351
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
352
        return "no signature"
 
353
 
 
354
 
307
355
class LogGenerator(object):
308
356
    """A generator of log revisions."""
309
357
 
354
402
        # Tweak the LogRequest based on what the LogFormatter can handle.
355
403
        # (There's no point generating stuff if the formatter can't display it.)
356
404
        rqst = self.rqst
357
 
        rqst['levels'] = lf.get_levels()
 
405
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
406
            # user didn't specify levels, use whatever the LF can handle:
 
407
            rqst['levels'] = lf.get_levels()
 
408
 
358
409
        if not getattr(lf, 'supports_tags', False):
359
410
            rqst['generate_tags'] = False
360
411
        if not getattr(lf, 'supports_delta', False):
361
412
            rqst['delta_type'] = None
362
413
        if not getattr(lf, 'supports_diff', False):
363
414
            rqst['diff_type'] = None
 
415
        if not getattr(lf, 'supports_signatures', False):
 
416
            rqst['signature'] = False
364
417
 
365
418
        # Find and print the interesting revisions
366
419
        generator = self._generator_factory(self.branch, rqst)
370
423
 
371
424
    def _generator_factory(self, branch, rqst):
372
425
        """Make the LogGenerator object to use.
373
 
        
 
426
 
374
427
        Subclasses may wish to override this.
375
428
        """
376
429
        return _DefaultLogGenerator(branch, rqst)
400
453
        levels = rqst.get('levels')
401
454
        limit = rqst.get('limit')
402
455
        diff_type = rqst.get('diff_type')
 
456
        show_signature = rqst.get('signature')
 
457
        omit_merges = rqst.get('omit_merges')
403
458
        log_count = 0
404
459
        revision_iterator = self._create_log_revision_iterator()
405
460
        for revs in revision_iterator:
407
462
                # 0 levels means show everything; merge_depth counts from 0
408
463
                if levels != 0 and merge_depth >= levels:
409
464
                    continue
 
465
                if omit_merges and len(rev.parent_ids) > 1:
 
466
                    continue
410
467
                if diff_type is None:
411
468
                    diff = None
412
469
                else:
413
470
                    diff = self._format_diff(rev, rev_id, diff_type)
 
471
                if show_signature:
 
472
                    signature = format_signature_validity(rev_id,
 
473
                                                self.branch.repository)
 
474
                else:
 
475
                    signature = None
414
476
                yield LogRevision(rev, revno, merge_depth, delta,
415
 
                    self.rev_tag_dict.get(rev_id), diff)
 
477
                    self.rev_tag_dict.get(rev_id), diff, signature)
416
478
                if limit:
417
479
                    log_count += 1
418
480
                    if log_count >= limit:
432
494
        else:
433
495
            specific_files = None
434
496
        s = StringIO()
 
497
        path_encoding = get_diff_header_encoding()
435
498
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
436
 
            new_label='')
 
499
            new_label='', path_encoding=path_encoding)
437
500
        return s.getvalue()
438
501
 
439
502
    def _create_log_revision_iterator(self):
472
535
 
473
536
        # Apply the other filters
474
537
        return make_log_rev_iterator(self.branch, view_revisions,
475
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
538
            rqst.get('delta_type'), rqst.get('match'),
476
539
            file_ids=rqst.get('specific_fileids'),
477
540
            direction=rqst.get('direction'))
478
541
 
491
554
            rqst.get('specific_fileids')[0], view_revisions,
492
555
            include_merges=rqst.get('levels') != 1)
493
556
        return make_log_rev_iterator(self.branch, view_revisions,
494
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
557
            rqst.get('delta_type'), rqst.get('match'))
495
558
 
496
559
 
497
560
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
568
             a list of the same tuples.
506
569
    """
507
570
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
 
        raise errors.BzrCommandError(
509
 
            '--exclude-common-ancestry requires two different revisions')
 
571
        raise errors.BzrCommandError(gettext(
 
572
            '--exclude-common-ancestry requires two different revisions'))
510
573
    if direction not in ('reverse', 'forward'):
511
 
        raise ValueError('invalid direction %r' % direction)
 
574
        raise ValueError(gettext('invalid direction %r') % direction)
512
575
    br_revno, br_rev_id = branch.last_revision_info()
513
576
    if br_revno == 0:
514
577
        return []
522
585
    elif not generate_merge_revisions:
523
586
        # If we only want to see linear revisions, we can iterate ...
524
587
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
525
 
                                             direction)
 
588
                                             direction, exclude_common_ancestry)
526
589
        if direction == 'forward':
527
590
            iter_revs = reversed(iter_revs)
528
591
    else:
539
602
        # It's the tip
540
603
        return [(br_rev_id, br_revno, 0)]
541
604
    else:
542
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
543
 
        revno_str = '.'.join(str(n) for n in revno)
 
605
        revno_str = _compute_revno_str(branch, rev_id)
544
606
        return [(rev_id, revno_str, 0)]
545
607
 
546
608
 
547
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
548
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
 
609
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
 
610
                             exclude_common_ancestry=False):
 
611
    result = _linear_view_revisions(
 
612
        branch, start_rev_id, end_rev_id,
 
613
        exclude_common_ancestry=exclude_common_ancestry)
549
614
    # If a start limit was given and it's not obviously an
550
615
    # ancestor of the end limit, check it before outputting anything
551
616
    if direction == 'forward' or (start_rev_id
553
618
        try:
554
619
            result = list(result)
555
620
        except _StartNotLinearAncestor:
556
 
            raise errors.BzrCommandError('Start revision not found in'
557
 
                ' left-hand history of end revision.')
 
621
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
622
                ' left-hand history of end revision.'))
558
623
    return result
559
624
 
560
625
 
572
637
    if delayed_graph_generation:
573
638
        try:
574
639
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
640
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
641
                if _has_merges(branch, rev_id):
577
642
                    # The end_rev_id can be nested down somewhere. We need an
578
643
                    # explicit ancestry check. There is an ambiguity here as we
599
664
        except _StartNotLinearAncestor:
600
665
            # A merge was never detected so the lower revision limit can't
601
666
            # be nested down somewhere
602
 
            raise errors.BzrCommandError('Start revision not found in'
603
 
                ' history of end revision.')
 
667
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
668
                ' history of end revision.'))
604
669
 
605
670
    # We exit the loop above because we encounter a revision with merges, from
606
671
    # this revision, we need to switch to _graph_view_revisions.
623
688
    return len(parents) > 1
624
689
 
625
690
 
 
691
def _compute_revno_str(branch, rev_id):
 
692
    """Compute the revno string from a rev_id.
 
693
 
 
694
    :return: The revno string, or None if the revision is not in the supplied
 
695
        branch.
 
696
    """
 
697
    try:
 
698
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
699
    except errors.NoSuchRevision:
 
700
        # The revision must be outside of this branch
 
701
        return None
 
702
    else:
 
703
        return '.'.join(str(n) for n in revno)
 
704
 
 
705
 
626
706
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
707
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
628
708
    if start_rev_id and end_rev_id:
629
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
630
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
709
        try:
 
710
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
711
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
712
        except errors.NoSuchRevision:
 
713
            # one or both is not in the branch; not obvious
 
714
            return False
631
715
        if len(start_dotted) == 1 and len(end_dotted) == 1:
632
716
            # both on mainline
633
717
            return start_dotted[0] <= end_dotted[0]
643
727
    return True
644
728
 
645
729
 
646
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
730
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
731
                           exclude_common_ancestry=False):
647
732
    """Calculate a sequence of revisions to view, newest to oldest.
648
733
 
649
734
    :param start_rev_id: the lower revision-id
650
735
    :param end_rev_id: the upper revision-id
 
736
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
737
        the iterated revisions.
651
738
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
652
739
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
 
      is not found walking the left-hand history
 
740
        is not found walking the left-hand history
654
741
    """
655
742
    br_revno, br_rev_id = branch.last_revision_info()
656
743
    repo = branch.repository
 
744
    graph = repo.get_graph()
657
745
    if start_rev_id is None and end_rev_id is None:
658
746
        cur_revno = br_revno
659
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
747
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
748
            (_mod_revision.NULL_REVISION,)):
660
749
            yield revision_id, str(cur_revno), 0
661
750
            cur_revno -= 1
662
751
    else:
663
752
        if end_rev_id is None:
664
753
            end_rev_id = br_rev_id
665
754
        found_start = start_rev_id is None
666
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
667
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
668
 
            revno_str = '.'.join(str(n) for n in revno)
 
755
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
756
                (_mod_revision.NULL_REVISION,)):
 
757
            revno_str = _compute_revno_str(branch, revision_id)
669
758
            if not found_start and revision_id == start_rev_id:
670
 
                yield revision_id, revno_str, 0
 
759
                if not exclude_common_ancestry:
 
760
                    yield revision_id, revno_str, 0
671
761
                found_start = True
672
762
                break
673
763
            else:
721
811
            yield rev_id, '.'.join(map(str, revno)), merge_depth
722
812
 
723
813
 
724
 
@deprecated_function(deprecated_in((2, 2, 0)))
725
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
726
 
        specific_fileid, generate_merge_revisions):
727
 
    """Calculate the revisions to view.
728
 
 
729
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
730
 
             a list of the same tuples.
731
 
    """
732
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
733
 
        end_revision)
734
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
735
 
        direction, generate_merge_revisions or specific_fileid))
736
 
    if specific_fileid:
737
 
        view_revisions = _filter_revisions_touching_file_id(branch,
738
 
            specific_fileid, view_revisions,
739
 
            include_merges=generate_merge_revisions)
740
 
    return _rebase_merge_depth(view_revisions)
741
 
 
742
 
 
743
814
def _rebase_merge_depth(view_revisions):
744
815
    """Adjust depths upwards so the top level is 0."""
745
816
    # If either the first or last revision have a merge_depth of 0, we're done
789
860
    return log_rev_iterator
790
861
 
791
862
 
792
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
863
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
793
864
    """Create a filtered iterator of log_rev_iterator matching on a regex.
794
865
 
795
866
    :param branch: The branch being logged.
796
867
    :param generate_delta: Whether to generate a delta for each revision.
797
 
    :param search: A user text search string.
 
868
    :param match: A dictionary with properties as keys and lists of strings
 
869
        as values. To match, a revision may match any of the supplied strings
 
870
        within a single property but must match at least one string for each
 
871
        property.
798
872
    :param log_rev_iterator: An input iterator containing all revisions that
799
873
        could be displayed, in lists.
800
874
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
801
875
        delta).
802
876
    """
803
 
    if search is None:
 
877
    if match is None:
804
878
        return log_rev_iterator
805
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
806
 
            'log message filter')
807
 
    return _filter_message_re(searchRE, log_rev_iterator)
808
 
 
809
 
 
810
 
def _filter_message_re(searchRE, log_rev_iterator):
 
879
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
880
                for (k,v) in match.iteritems()]
 
881
    return _filter_re(searchRE, log_rev_iterator)
 
882
 
 
883
 
 
884
def _filter_re(searchRE, log_rev_iterator):
811
885
    for revs in log_rev_iterator:
812
 
        new_revs = []
813
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
814
 
            if searchRE.search(rev.message):
815
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
816
 
        yield new_revs
817
 
 
 
886
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
887
        if new_revs:
 
888
            yield new_revs
 
889
 
 
890
def _match_filter(searchRE, rev):
 
891
    strings = {
 
892
               'message': (rev.message,),
 
893
               'committer': (rev.committer,),
 
894
               'author': (rev.get_apparent_authors()),
 
895
               'bugs': list(rev.iter_bugs())
 
896
               }
 
897
    strings[''] = [item for inner_list in strings.itervalues()
 
898
                   for item in inner_list]
 
899
    for (k,v) in searchRE:
 
900
        if k in strings and not _match_any_filter(strings[k], v):
 
901
            return False
 
902
    return True
 
903
 
 
904
def _match_any_filter(strings, res):
 
905
    return any([filter(None, map(re.search, strings)) for re in res])
818
906
 
819
907
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
820
908
    fileids=None, direction='reverse'):
893
981
 
894
982
def _update_fileids(delta, fileids, stop_on):
895
983
    """Update the set of file-ids to search based on file lifecycle events.
896
 
    
 
984
 
897
985
    :param fileids: a set of fileids to update
898
986
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
899
987
      fileids set once their add or remove entry is detected respectively
940
1028
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
941
1029
        delta).
942
1030
    """
943
 
    repository = branch.repository
944
1031
    num = 9
945
1032
    for batch in log_rev_iterator:
946
1033
        batch = iter(batch)
995
1082
    if branch_revno != 0:
996
1083
        if (start_rev_id == _mod_revision.NULL_REVISION
997
1084
            or end_rev_id == _mod_revision.NULL_REVISION):
998
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1085
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
999
1086
        if start_revno > end_revno:
1000
 
            raise errors.BzrCommandError("Start revision must be older than "
1001
 
                                         "the end revision.")
 
1087
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1088
                                         "older than the end revision."))
1002
1089
    return (start_rev_id, end_rev_id)
1003
1090
 
1004
1091
 
1053
1140
 
1054
1141
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1055
1142
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1056
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1143
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1057
1144
    if start_revno > end_revno:
1058
 
        raise errors.BzrCommandError("Start revision must be older than "
1059
 
                                     "the end revision.")
 
1145
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1146
                                     "than the end revision."))
1060
1147
 
1061
1148
    if end_revno < start_revno:
1062
1149
        return None, None, None, None
1063
1150
    cur_revno = branch_revno
1064
1151
    rev_nos = {}
1065
1152
    mainline_revs = []
1066
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1067
 
                        branch_last_revision):
 
1153
    graph = branch.repository.get_graph()
 
1154
    for revision_id in graph.iter_lefthand_ancestry(
 
1155
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1156
        if cur_revno < start_revno:
1069
1157
            # We have gone far enough, but we always add 1 more revision
1070
1158
            rev_nos[revision_id] = cur_revno
1084
1172
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1085
1173
 
1086
1174
 
1087
 
@deprecated_function(deprecated_in((2, 2, 0)))
1088
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1089
 
    """Filter view_revisions based on revision ranges.
1090
 
 
1091
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1092
 
            tuples to be filtered.
1093
 
 
1094
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1095
 
            If NONE then all revisions up to the end_rev_id are logged.
1096
 
 
1097
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1098
 
            If NONE then all revisions up to the end of the log are logged.
1099
 
 
1100
 
    :return: The filtered view_revisions.
1101
 
    """
1102
 
    if start_rev_id or end_rev_id:
1103
 
        revision_ids = [r for r, n, d in view_revisions]
1104
 
        if start_rev_id:
1105
 
            start_index = revision_ids.index(start_rev_id)
1106
 
        else:
1107
 
            start_index = 0
1108
 
        if start_rev_id == end_rev_id:
1109
 
            end_index = start_index
1110
 
        else:
1111
 
            if end_rev_id:
1112
 
                end_index = revision_ids.index(end_rev_id)
1113
 
            else:
1114
 
                end_index = len(view_revisions) - 1
1115
 
        # To include the revisions merged into the last revision,
1116
 
        # extend end_rev_id down to, but not including, the next rev
1117
 
        # with the same or lesser merge_depth
1118
 
        end_merge_depth = view_revisions[end_index][2]
1119
 
        try:
1120
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1121
 
                if view_revisions[index][2] <= end_merge_depth:
1122
 
                    end_index = index - 1
1123
 
                    break
1124
 
        except IndexError:
1125
 
            # if the search falls off the end then log to the end as well
1126
 
            end_index = len(view_revisions) - 1
1127
 
        view_revisions = view_revisions[start_index:end_index+1]
1128
 
    return view_revisions
1129
 
 
1130
 
 
1131
1175
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1132
1176
    include_merges=True):
1133
1177
    r"""Return the list of revision ids which touch a given file id.
1136
1180
    This includes the revisions which directly change the file id,
1137
1181
    and the revisions which merge these changes. So if the
1138
1182
    revision graph is::
 
1183
 
1139
1184
        A-.
1140
1185
        |\ \
1141
1186
        B C E
1168
1213
    """
1169
1214
    # Lookup all possible text keys to determine which ones actually modified
1170
1215
    # the file.
 
1216
    graph = branch.repository.get_file_graph()
 
1217
    get_parent_map = graph.get_parent_map
1171
1218
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1219
    next_keys = None
1173
1220
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1224
    #       indexing layer. We might consider passing in hints as to the known
1178
1225
    #       access pattern (sparse/clustered, high success rate/low success
1179
1226
    #       rate). This particular access is clustered with a low success rate.
1180
 
    get_parent_map = branch.repository.texts.get_parent_map
1181
1227
    modified_text_revisions = set()
1182
1228
    chunk_size = 1000
1183
1229
    for start in xrange(0, len(text_keys), chunk_size):
1210
1256
    return result
1211
1257
 
1212
1258
 
1213
 
@deprecated_function(deprecated_in((2, 2, 0)))
1214
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1215
 
                       include_merges=True):
1216
 
    """Produce an iterator of revisions to show
1217
 
    :return: an iterator of (revision_id, revno, merge_depth)
1218
 
    (if there is no revno for a revision, None is supplied)
1219
 
    """
1220
 
    if not include_merges:
1221
 
        revision_ids = mainline_revs[1:]
1222
 
        if direction == 'reverse':
1223
 
            revision_ids.reverse()
1224
 
        for revision_id in revision_ids:
1225
 
            yield revision_id, str(rev_nos[revision_id]), 0
1226
 
        return
1227
 
    graph = branch.repository.get_graph()
1228
 
    # This asks for all mainline revisions, which means we only have to spider
1229
 
    # sideways, rather than depth history. That said, its still size-of-history
1230
 
    # and should be addressed.
1231
 
    # mainline_revisions always includes an extra revision at the beginning, so
1232
 
    # don't request it.
1233
 
    parent_map = dict(((key, value) for key, value in
1234
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1235
 
    # filter out ghosts; merge_sort errors on ghosts.
1236
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1237
 
    merge_sorted_revisions = tsort.merge_sort(
1238
 
        rev_graph,
1239
 
        mainline_revs[-1],
1240
 
        mainline_revs,
1241
 
        generate_revno=True)
1242
 
 
1243
 
    if direction == 'forward':
1244
 
        # forward means oldest first.
1245
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1246
 
    elif direction != 'reverse':
1247
 
        raise ValueError('invalid direction %r' % direction)
1248
 
 
1249
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1250
 
         ) in merge_sorted_revisions:
1251
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1252
 
 
1253
 
 
1254
1259
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1255
1260
    """Reverse revisions by depth.
1256
1261
 
1291
1296
    """
1292
1297
 
1293
1298
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
 
                 tags=None, diff=None):
 
1299
                 tags=None, diff=None, signature=None):
1295
1300
        self.rev = rev
1296
 
        self.revno = str(revno)
 
1301
        if revno is None:
 
1302
            self.revno = None
 
1303
        else:
 
1304
            self.revno = str(revno)
1297
1305
        self.merge_depth = merge_depth
1298
1306
        self.delta = delta
1299
1307
        self.tags = tags
1300
1308
        self.diff = diff
 
1309
        self.signature = signature
1301
1310
 
1302
1311
 
1303
1312
class LogFormatter(object):
1312
1321
    to indicate which LogRevision attributes it supports:
1313
1322
 
1314
1323
    - supports_delta must be True if this log formatter supports delta.
1315
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1316
 
        attribute describes whether the 'short_status' format (1) or the long
1317
 
        one (2) should be used.
 
1324
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1325
      attribute describes whether the 'short_status' format (1) or the long
 
1326
      one (2) should be used.
1318
1327
 
1319
1328
    - supports_merge_revisions must be True if this log formatter supports
1320
 
        merge revisions.  If not, then only mainline revisions will be passed
1321
 
        to the formatter.
 
1329
      merge revisions.  If not, then only mainline revisions will be passed
 
1330
      to the formatter.
1322
1331
 
1323
1332
    - preferred_levels is the number of levels this formatter defaults to.
1324
 
        The default value is zero meaning display all levels.
1325
 
        This value is only relevant if supports_merge_revisions is True.
 
1333
      The default value is zero meaning display all levels.
 
1334
      This value is only relevant if supports_merge_revisions is True.
1326
1335
 
1327
1336
    - supports_tags must be True if this log formatter supports tags.
1328
 
        Otherwise the tags attribute may not be populated.
 
1337
      Otherwise the tags attribute may not be populated.
1329
1338
 
1330
1339
    - supports_diff must be True if this log formatter supports diffs.
1331
 
        Otherwise the diff attribute may not be populated.
 
1340
      Otherwise the diff attribute may not be populated.
 
1341
 
 
1342
    - supports_signatures must be True if this log formatter supports GPG
 
1343
      signatures.
1332
1344
 
1333
1345
    Plugins can register functions to show custom revision properties using
1334
1346
    the properties_handler_registry. The registered function
1335
 
    must respect the following interface description:
 
1347
    must respect the following interface description::
 
1348
 
1336
1349
        def my_show_properties(properties_dict):
1337
1350
            # code that returns a dict {'name':'value'} of the properties
1338
1351
            # to be shown
1341
1354
 
1342
1355
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1356
                 delta_format=None, levels=None, show_advice=False,
1344
 
                 to_exact_file=None):
 
1357
                 to_exact_file=None, author_list_handler=None):
1345
1358
        """Create a LogFormatter.
1346
1359
 
1347
1360
        :param to_file: the file to output to
1348
 
        :param to_exact_file: if set, gives an output stream to which 
 
1361
        :param to_exact_file: if set, gives an output stream to which
1349
1362
             non-Unicode diffs are written.
1350
1363
        :param show_ids: if True, revision-ids are to be displayed
1351
1364
        :param show_timezone: the timezone to use
1355
1368
          let the log formatter decide.
1356
1369
        :param show_advice: whether to show advice at the end of the
1357
1370
          log or not
 
1371
        :param author_list_handler: callable generating a list of
 
1372
          authors to display for a given revision
1358
1373
        """
1359
1374
        self.to_file = to_file
1360
1375
        # 'exact' stream used to show diff, it should print content 'as is'
1375
1390
        self.levels = levels
1376
1391
        self._show_advice = show_advice
1377
1392
        self._merge_count = 0
 
1393
        self._author_list_handler = author_list_handler
1378
1394
 
1379
1395
    def get_levels(self):
1380
1396
        """Get the number of levels to display or 0 for all."""
1399
1415
            if advice_sep:
1400
1416
                self.to_file.write(advice_sep)
1401
1417
            self.to_file.write(
1402
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1418
                "Use --include-merged or -n0 to see merged revisions.\n")
1403
1419
 
1404
1420
    def get_advice_separator(self):
1405
1421
        """Get the text separating the log from the closing advice."""
1412
1428
        return address
1413
1429
 
1414
1430
    def short_author(self, rev):
1415
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1416
 
        if name:
1417
 
            return name
1418
 
        return address
 
1431
        return self.authors(rev, 'first', short=True, sep=', ')
 
1432
 
 
1433
    def authors(self, rev, who, short=False, sep=None):
 
1434
        """Generate list of authors, taking --authors option into account.
 
1435
 
 
1436
        The caller has to specify the name of a author list handler,
 
1437
        as provided by the author list registry, using the ``who``
 
1438
        argument.  That name only sets a default, though: when the
 
1439
        user selected a different author list generation using the
 
1440
        ``--authors`` command line switch, as represented by the
 
1441
        ``author_list_handler`` constructor argument, that value takes
 
1442
        precedence.
 
1443
 
 
1444
        :param rev: The revision for which to generate the list of authors.
 
1445
        :param who: Name of the default handler.
 
1446
        :param short: Whether to shorten names to either name or address.
 
1447
        :param sep: What separator to use for automatic concatenation.
 
1448
        """
 
1449
        if self._author_list_handler is not None:
 
1450
            # The user did specify --authors, which overrides the default
 
1451
            author_list_handler = self._author_list_handler
 
1452
        else:
 
1453
            # The user didn't specify --authors, so we use the caller's default
 
1454
            author_list_handler = author_list_registry.get(who)
 
1455
        names = author_list_handler(rev)
 
1456
        if short:
 
1457
            for i in range(len(names)):
 
1458
                name, address = config.parse_username(names[i])
 
1459
                if name:
 
1460
                    names[i] = name
 
1461
                else:
 
1462
                    names[i] = address
 
1463
        if sep is not None:
 
1464
            names = sep.join(names)
 
1465
        return names
1419
1466
 
1420
1467
    def merge_marker(self, revision):
1421
1468
        """Get the merge marker to include in the output or '' if none."""
1491
1538
    supports_delta = True
1492
1539
    supports_tags = True
1493
1540
    supports_diff = True
 
1541
    supports_signatures = True
1494
1542
 
1495
1543
    def __init__(self, *args, **kwargs):
1496
1544
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1516
1564
                self.merge_marker(revision)))
1517
1565
        if revision.tags:
1518
1566
            lines.append('tags: %s' % (', '.join(revision.tags)))
1519
 
        if self.show_ids:
 
1567
        if self.show_ids or revision.revno is None:
1520
1568
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1569
        if self.show_ids:
1521
1570
            for parent_id in revision.rev.parent_ids:
1522
1571
                lines.append('parent: %s' % (parent_id,))
1523
1572
        lines.extend(self.custom_properties(revision.rev))
1524
1573
 
1525
1574
        committer = revision.rev.committer
1526
 
        authors = revision.rev.get_apparent_authors()
 
1575
        authors = self.authors(revision.rev, 'all')
1527
1576
        if authors != [committer]:
1528
1577
            lines.append('author: %s' % (", ".join(authors),))
1529
1578
        lines.append('committer: %s' % (committer,))
1534
1583
 
1535
1584
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1536
1585
 
 
1586
        if revision.signature is not None:
 
1587
            lines.append('signature: ' + revision.signature)
 
1588
 
1537
1589
        lines.append('message:')
1538
1590
        if not revision.rev.message:
1539
1591
            lines.append('  (no message)')
1548
1600
        if revision.delta is not None:
1549
1601
            # Use the standard status output to display changes
1550
1602
            from bzrlib.delta import report_delta
1551
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1603
            report_delta(to_file, revision.delta, short_status=False,
1552
1604
                         show_ids=self.show_ids, indent=indent)
1553
1605
        if revision.diff is not None:
1554
1606
            to_file.write(indent + 'diff:\n')
1586
1638
        indent = '    ' * depth
1587
1639
        revno_width = self.revno_width_by_depth.get(depth)
1588
1640
        if revno_width is None:
1589
 
            if revision.revno.find('.') == -1:
 
1641
            if revision.revno is None or revision.revno.find('.') == -1:
1590
1642
                # mainline revno, e.g. 12345
1591
1643
                revno_width = 5
1592
1644
            else:
1600
1652
        if revision.tags:
1601
1653
            tags = ' {%s}' % (', '.join(revision.tags))
1602
1654
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
 
                revision.revno, self.short_author(revision.rev),
 
1655
                revision.revno or "", self.short_author(revision.rev),
1604
1656
                format_date(revision.rev.timestamp,
1605
1657
                            revision.rev.timezone or 0,
1606
1658
                            self.show_timezone, date_fmt="%Y-%m-%d",
1607
1659
                            show_offset=False),
1608
1660
                tags, self.merge_marker(revision)))
1609
1661
        self.show_properties(revision.rev, indent+offset)
1610
 
        if self.show_ids:
 
1662
        if self.show_ids or revision.revno is None:
1611
1663
            to_file.write(indent + offset + 'revision-id:%s\n'
1612
1664
                          % (revision.rev.revision_id,))
1613
1665
        if not revision.rev.message:
1620
1672
        if revision.delta is not None:
1621
1673
            # Use the standard status output to display changes
1622
1674
            from bzrlib.delta import report_delta
1623
 
            report_delta(to_file, revision.delta, 
1624
 
                         short_status=self.delta_format==1, 
 
1675
            report_delta(to_file, revision.delta,
 
1676
                         short_status=self.delta_format==1,
1625
1677
                         show_ids=self.show_ids, indent=indent + offset)
1626
1678
        if revision.diff is not None:
1627
1679
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1666
1718
 
1667
1719
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1720
        """Format log info into one string. Truncate tail of string
1669
 
        :param  revno:      revision number or None.
1670
 
                            Revision numbers counts from 1.
1671
 
        :param  rev:        revision object
1672
 
        :param  max_chars:  maximum length of resulting string
1673
 
        :param  tags:       list of tags or None
1674
 
        :param  prefix:     string to prefix each line
1675
 
        :return:            formatted truncated string
 
1721
 
 
1722
        :param revno:      revision number or None.
 
1723
                           Revision numbers counts from 1.
 
1724
        :param rev:        revision object
 
1725
        :param max_chars:  maximum length of resulting string
 
1726
        :param tags:       list of tags or None
 
1727
        :param prefix:     string to prefix each line
 
1728
        :return:           formatted truncated string
1676
1729
        """
1677
1730
        out = []
1678
1731
        if revno:
1679
1732
            # show revno only when is not None
1680
1733
            out.append("%s:" % revno)
1681
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1734
        if max_chars is not None:
 
1735
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1736
        else:
 
1737
            out.append(self.short_author(rev))
1682
1738
        out.append(self.date_string(rev))
1683
1739
        if len(rev.parent_ids) > 1:
1684
1740
            out.append('[merge]')
1703
1759
                               self.show_timezone,
1704
1760
                               date_fmt='%Y-%m-%d',
1705
1761
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
 
1762
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1763
        committer_str = committer_str.replace(' <', '  <')
1707
1764
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1708
1765
 
1709
1766
        if revision.delta is not None and revision.delta.has_changed():
1742
1799
        return self.get(name)(*args, **kwargs)
1743
1800
 
1744
1801
    def get_default(self, branch):
1745
 
        return self.get(branch.get_config().log_format())
 
1802
        c = branch.get_config_stack()
 
1803
        return self.get(c.get('log_format'))
1746
1804
 
1747
1805
 
1748
1806
log_formatter_registry = LogFormatterRegistry()
1749
1807
 
1750
1808
 
1751
1809
log_formatter_registry.register('short', ShortLogFormatter,
1752
 
                                'Moderately short log format')
 
1810
                                'Moderately short log format.')
1753
1811
log_formatter_registry.register('long', LongLogFormatter,
1754
 
                                'Detailed log format')
 
1812
                                'Detailed log format.')
1755
1813
log_formatter_registry.register('line', LineLogFormatter,
1756
 
                                'Log format with one line per revision')
 
1814
                                'Log format with one line per revision.')
1757
1815
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1758
 
                                'Format used by GNU ChangeLog files')
 
1816
                                'Format used by GNU ChangeLog files.')
1759
1817
 
1760
1818
 
1761
1819
def register_formatter(name, formatter):
1771
1829
    try:
1772
1830
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1773
1831
    except KeyError:
1774
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1775
 
 
1776
 
 
1777
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
 
    # deprecated; for compatibility
1779
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1780
 
    lf.show(revno, rev, delta)
 
1832
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1833
 
 
1834
 
 
1835
def author_list_all(rev):
 
1836
    return rev.get_apparent_authors()[:]
 
1837
 
 
1838
 
 
1839
def author_list_first(rev):
 
1840
    lst = rev.get_apparent_authors()
 
1841
    try:
 
1842
        return [lst[0]]
 
1843
    except IndexError:
 
1844
        return []
 
1845
 
 
1846
 
 
1847
def author_list_committer(rev):
 
1848
    return [rev.committer]
 
1849
 
 
1850
 
 
1851
author_list_registry = registry.Registry()
 
1852
 
 
1853
author_list_registry.register('all', author_list_all,
 
1854
                              'All authors')
 
1855
 
 
1856
author_list_registry.register('first', author_list_first,
 
1857
                              'The first author')
 
1858
 
 
1859
author_list_registry.register('committer', author_list_committer,
 
1860
                              'The committer')
1781
1861
 
1782
1862
 
1783
1863
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1848
1928
    old_revisions = set()
1849
1929
    new_history = []
1850
1930
    new_revisions = set()
1851
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1852
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1931
    graph = repository.get_graph()
 
1932
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1933
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1853
1934
    stop_revision = None
1854
1935
    do_old = True
1855
1936
    do_new = True
1930
2011
        lf.log_revision(lr)
1931
2012
 
1932
2013
 
1933
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2014
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2015
    """Find file-ids and kinds given a list of files and a revision range.
1935
2016
 
1936
2017
    We search for files at the end of the range. If not found there,
1940
2021
    :param file_list: the list of paths given on the command line;
1941
2022
      the first of these can be a branch location or a file path,
1942
2023
      the remainder must be file paths
 
2024
    :param add_cleanup: When the branch returned is read locked,
 
2025
      an unlock call will be queued to the cleanup.
1943
2026
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2027
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
2028
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2029
      branch will be read-locked.
1947
2030
    """
1948
 
    from builtins import _get_revision_range, safe_relpath_files
1949
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
 
2031
    from builtins import _get_revision_range
 
2032
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2033
        file_list[0])
 
2034
    add_cleanup(b.lock_read().unlock)
1951
2035
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
2036
    # those paths might be deleted ones, they might be on a case-insensitive
1953
2037
    # filesystem and/or they might be in silly locations (like another branch).
1957
2041
    # case of running log in a nested directory, assuming paths beyond the
1958
2042
    # first one haven't been deleted ...
1959
2043
    if tree:
1960
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2044
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1961
2045
    else:
1962
2046
        relpaths = [path] + file_list[1:]
1963
2047
    info_list = []
2041
2125
                          len(row) > 1 and row[1] == 'fixed']
2042
2126
 
2043
2127
        if fixed_bug_urls:
2044
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2128
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2129
                    ' '.join(fixed_bug_urls)}
2045
2130
    return {}
2046
2131
 
2047
2132
properties_handler_registry.register('bugs_properties_handler',