~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-07-09 00:44:25 UTC
  • mfrom: (4454.3.79 1.17-rework-annotate)
  • Revision ID: pqm@pqm.ubuntu.com-20090709004425-6tukiuklys5y02m5
(jam) Rework the internals of annotate to use a class object (fixes
        bug #387952)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2009 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
73
73
    repository as _mod_repository,
74
74
    revision as _mod_revision,
75
75
    revisionspec,
 
76
    trace,
76
77
    tsort,
77
 
    i18n,
78
78
    )
79
79
""")
80
80
 
81
81
from bzrlib import (
82
 
    lazy_regex,
83
82
    registry,
84
83
    )
85
84
from bzrlib.osutils import (
86
85
    format_date,
87
 
    format_date_with_offset_in_original_timezone,
88
 
    get_diff_header_encoding,
89
86
    get_terminal_encoding,
 
87
    re_compile_checked,
90
88
    terminal_width,
91
89
    )
92
 
from bzrlib.symbol_versioning import (
93
 
    deprecated_function,
94
 
    deprecated_in,
95
 
    )
96
90
 
97
91
 
98
92
def find_touching_revisions(branch, file_id):
110
104
    last_path = None
111
105
    revno = 1
112
106
    for revision_id in branch.revision_history():
113
 
        this_inv = branch.repository.get_inventory(revision_id)
114
 
        if this_inv.has_id(file_id):
 
107
        this_inv = branch.repository.get_revision_inventory(revision_id)
 
108
        if file_id in this_inv:
115
109
            this_ie = this_inv[file_id]
116
110
            this_path = this_inv.id2path(file_id)
117
111
        else:
221
215
    'direction': 'reverse',
222
216
    'levels': 1,
223
217
    'generate_tags': True,
224
 
    'exclude_common_ancestry': False,
225
218
    '_match_using_deltas': True,
226
219
    }
227
220
 
228
221
 
229
222
def make_log_request_dict(direction='reverse', specific_fileids=None,
230
 
                          start_revision=None, end_revision=None, limit=None,
231
 
                          message_search=None, levels=1, generate_tags=True,
232
 
                          delta_type=None,
233
 
                          diff_type=None, _match_using_deltas=True,
234
 
                          exclude_common_ancestry=False,
235
 
                          signature=False,
236
 
                          ):
 
223
    start_revision=None, end_revision=None, limit=None,
 
224
    message_search=None, levels=1, generate_tags=True, delta_type=None,
 
225
    diff_type=None, _match_using_deltas=True):
237
226
    """Convenience function for making a logging request dictionary.
238
227
 
239
228
    Using this function may make code slightly safer by ensuring
262
251
      generate; 1 for just the mainline; 0 for all levels.
263
252
 
264
253
    :param generate_tags: If True, include tags for matched revisions.
265
 
`
 
254
 
266
255
    :param delta_type: Either 'full', 'partial' or None.
267
256
      'full' means generate the complete delta - adds/deletes/modifies/etc;
268
257
      'partial' means filter the delta using specific_fileids;
277
266
      algorithm used for matching specific_fileids. This parameter
278
267
      may be removed in the future so bzrlib client code should NOT
279
268
      use it.
280
 
 
281
 
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
282
 
      range operator or as a graph difference.
283
 
 
284
 
    :param signature: show digital signature information
285
269
    """
286
270
    return {
287
271
        'direction': direction,
294
278
        'generate_tags': generate_tags,
295
279
        'delta_type': delta_type,
296
280
        'diff_type': diff_type,
297
 
        'exclude_common_ancestry': exclude_common_ancestry,
298
 
        'signature': signature,
299
281
        # Add 'private' attributes for features that may be deprecated
300
282
        '_match_using_deltas': _match_using_deltas,
301
283
    }
303
285
 
304
286
def _apply_log_request_defaults(rqst):
305
287
    """Apply default values to a request dictionary."""
306
 
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
288
    result = _DEFAULT_REQUEST_PARAMS
307
289
    if rqst:
308
290
        result.update(rqst)
309
291
    return result
310
292
 
311
293
 
312
 
def format_signature_validity(rev_id, repo):
313
 
    """get the signature validity
314
 
    
315
 
    :param rev_id: revision id to validate
316
 
    :param repo: repository of revision
317
 
    :return: human readable string to print to log
318
 
    """
319
 
    from bzrlib import gpg
320
 
 
321
 
    gpg_strategy = gpg.GPGStrategy(None)
322
 
    result = repo.verify_revision(rev_id, gpg_strategy)
323
 
    if result[0] == gpg.SIGNATURE_VALID:
324
 
        return "valid signature from {0}".format(result[1])
325
 
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
326
 
        return "unknown key {0}".format(result[1])
327
 
    if result[0] == gpg.SIGNATURE_NOT_VALID:
328
 
        return "invalid signature!"
329
 
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
330
 
        return "no signature"
331
 
 
332
 
 
333
294
class LogGenerator(object):
334
295
    """A generator of log revisions."""
335
296
 
342
303
 
343
304
 
344
305
class Logger(object):
345
 
    """An object that generates, formats and displays a log."""
 
306
    """An object the generates, formats and displays a log."""
346
307
 
347
308
    def __init__(self, branch, rqst):
348
309
        """Create a Logger.
387
348
            rqst['delta_type'] = None
388
349
        if not getattr(lf, 'supports_diff', False):
389
350
            rqst['diff_type'] = None
390
 
        if not getattr(lf, 'supports_signatures', False):
391
 
            rqst['signature'] = False
392
351
 
393
352
        # Find and print the interesting revisions
394
353
        generator = self._generator_factory(self.branch, rqst)
425
384
        :return: An iterator yielding LogRevision objects.
426
385
        """
427
386
        rqst = self.rqst
428
 
        levels = rqst.get('levels')
429
 
        limit = rqst.get('limit')
430
 
        diff_type = rqst.get('diff_type')
431
 
        show_signature = rqst.get('signature')
432
387
        log_count = 0
433
388
        revision_iterator = self._create_log_revision_iterator()
434
389
        for revs in revision_iterator:
435
390
            for (rev_id, revno, merge_depth), rev, delta in revs:
436
391
                # 0 levels means show everything; merge_depth counts from 0
 
392
                levels = rqst.get('levels')
437
393
                if levels != 0 and merge_depth >= levels:
438
394
                    continue
439
 
                if diff_type is None:
440
 
                    diff = None
441
 
                else:
442
 
                    diff = self._format_diff(rev, rev_id, diff_type)
443
 
                if show_signature:
444
 
                    signature = format_signature_validity(rev_id,
445
 
                                                self.branch.repository)
446
 
                else:
447
 
                    signature = None
 
395
                diff = self._format_diff(rev, rev_id)
448
396
                yield LogRevision(rev, revno, merge_depth, delta,
449
 
                    self.rev_tag_dict.get(rev_id), diff, signature)
 
397
                    self.rev_tag_dict.get(rev_id), diff)
 
398
                limit = rqst.get('limit')
450
399
                if limit:
451
400
                    log_count += 1
452
401
                    if log_count >= limit:
453
402
                        return
454
403
 
455
 
    def _format_diff(self, rev, rev_id, diff_type):
 
404
    def _format_diff(self, rev, rev_id):
 
405
        diff_type = self.rqst.get('diff_type')
 
406
        if diff_type is None:
 
407
            return None
456
408
        repo = self.branch.repository
457
409
        if len(rev.parent_ids) == 0:
458
410
            ancestor_id = _mod_revision.NULL_REVISION
466
418
        else:
467
419
            specific_files = None
468
420
        s = StringIO()
469
 
        path_encoding = get_diff_header_encoding()
470
421
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
471
 
            new_label='', path_encoding=path_encoding)
 
422
            new_label='')
472
423
        return s.getvalue()
473
424
 
474
425
    def _create_log_revision_iterator(self):
498
449
        generate_merge_revisions = rqst.get('levels') != 1
499
450
        delayed_graph_generation = not rqst.get('specific_fileids') and (
500
451
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
501
 
        view_revisions = _calc_view_revisions(
502
 
            self.branch, self.start_rev_id, self.end_rev_id,
503
 
            rqst.get('direction'),
504
 
            generate_merge_revisions=generate_merge_revisions,
505
 
            delayed_graph_generation=delayed_graph_generation,
506
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
452
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
453
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
 
454
            delayed_graph_generation=delayed_graph_generation)
507
455
 
508
456
        # Apply the other filters
509
457
        return make_log_rev_iterator(self.branch, view_revisions,
516
464
        # Note that we always generate the merge revisions because
517
465
        # filter_revisions_touching_file_id() requires them ...
518
466
        rqst = self.rqst
519
 
        view_revisions = _calc_view_revisions(
520
 
            self.branch, self.start_rev_id, self.end_rev_id,
521
 
            rqst.get('direction'), generate_merge_revisions=True,
522
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
467
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
468
            self.end_rev_id, rqst.get('direction'), True)
523
469
        if not isinstance(view_revisions, list):
524
470
            view_revisions = list(view_revisions)
525
471
        view_revisions = _filter_revisions_touching_file_id(self.branch,
530
476
 
531
477
 
532
478
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
533
 
                         generate_merge_revisions,
534
 
                         delayed_graph_generation=False,
535
 
                         exclude_common_ancestry=False,
536
 
                         ):
 
479
    generate_merge_revisions, delayed_graph_generation=False):
537
480
    """Calculate the revisions to view.
538
481
 
539
482
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
540
483
             a list of the same tuples.
541
484
    """
542
 
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
543
 
        raise errors.BzrCommandError(
544
 
            '--exclude-common-ancestry requires two different revisions')
545
 
    if direction not in ('reverse', 'forward'):
546
 
        raise ValueError('invalid direction %r' % direction)
547
485
    br_revno, br_rev_id = branch.last_revision_info()
548
486
    if br_revno == 0:
549
487
        return []
550
488
 
551
 
    if (end_rev_id and start_rev_id == end_rev_id
552
 
        and (not generate_merge_revisions
553
 
             or not _has_merges(branch, end_rev_id))):
554
 
        # If a single revision is requested, check we can handle it
555
 
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
556
 
                                           br_revno)
557
 
    elif not generate_merge_revisions:
558
 
        # If we only want to see linear revisions, we can iterate ...
559
 
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
560
 
                                             direction, exclude_common_ancestry)
561
 
        if direction == 'forward':
562
 
            iter_revs = reversed(iter_revs)
 
489
    # If a single revision is requested, check we can handle it
 
490
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
 
491
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
 
492
    if generate_single_revision:
 
493
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
 
494
 
 
495
    # If we only want to see linear revisions, we can iterate ...
 
496
    if not generate_merge_revisions:
 
497
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
498
            direction)
563
499
    else:
564
 
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
565
 
                                            direction, delayed_graph_generation,
566
 
                                            exclude_common_ancestry)
567
 
        if direction == 'forward':
568
 
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
569
 
    return iter_revs
 
500
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
501
            direction, delayed_graph_generation)
570
502
 
571
503
 
572
504
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
574
506
        # It's the tip
575
507
        return [(br_rev_id, br_revno, 0)]
576
508
    else:
577
 
        revno_str = _compute_revno_str(branch, rev_id)
 
509
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
510
        revno_str = '.'.join(str(n) for n in revno)
578
511
        return [(rev_id, revno_str, 0)]
579
512
 
580
513
 
581
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
582
 
                             exclude_common_ancestry=False):
583
 
    result = _linear_view_revisions(
584
 
        branch, start_rev_id, end_rev_id,
585
 
        exclude_common_ancestry=exclude_common_ancestry)
 
514
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
 
515
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
586
516
    # If a start limit was given and it's not obviously an
587
517
    # ancestor of the end limit, check it before outputting anything
588
518
    if direction == 'forward' or (start_rev_id
592
522
        except _StartNotLinearAncestor:
593
523
            raise errors.BzrCommandError('Start revision not found in'
594
524
                ' left-hand history of end revision.')
 
525
    if direction == 'forward':
 
526
        result = reversed(result)
595
527
    return result
596
528
 
597
529
 
598
530
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
599
 
                            delayed_graph_generation,
600
 
                            exclude_common_ancestry=False):
 
531
    delayed_graph_generation):
601
532
    # On large trees, generating the merge graph can take 30-60 seconds
602
533
    # so we delay doing it until a merge is detected, incrementally
603
534
    # returning initial (non-merge) revisions while we can.
604
 
 
605
 
    # The above is only true for old formats (<= 0.92), for newer formats, a
606
 
    # couple of seconds only should be needed to load the whole graph and the
607
 
    # other graph operations needed are even faster than that -- vila 100201
608
535
    initial_revisions = []
609
536
    if delayed_graph_generation:
610
537
        try:
611
 
            for rev_id, revno, depth in  _linear_view_revisions(
612
 
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
538
            for rev_id, revno, depth in \
 
539
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
613
540
                if _has_merges(branch, rev_id):
614
 
                    # The end_rev_id can be nested down somewhere. We need an
615
 
                    # explicit ancestry check. There is an ambiguity here as we
616
 
                    # may not raise _StartNotLinearAncestor for a revision that
617
 
                    # is an ancestor but not a *linear* one. But since we have
618
 
                    # loaded the graph to do the check (or calculate a dotted
619
 
                    # revno), we may as well accept to show the log...  We need
620
 
                    # the check only if start_rev_id is not None as all
621
 
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
622
 
                    # -- vila 20100319
623
 
                    graph = branch.repository.get_graph()
624
 
                    if (start_rev_id is not None
625
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
626
 
                        raise _StartNotLinearAncestor()
627
 
                    # Since we collected the revisions so far, we need to
628
 
                    # adjust end_rev_id.
629
541
                    end_rev_id = rev_id
630
542
                    break
631
543
                else:
632
544
                    initial_revisions.append((rev_id, revno, depth))
633
545
            else:
634
546
                # No merged revisions found
635
 
                return initial_revisions
 
547
                if direction == 'reverse':
 
548
                    return initial_revisions
 
549
                elif direction == 'forward':
 
550
                    return reversed(initial_revisions)
 
551
                else:
 
552
                    raise ValueError('invalid direction %r' % direction)
636
553
        except _StartNotLinearAncestor:
637
554
            # A merge was never detected so the lower revision limit can't
638
555
            # be nested down somewhere
639
556
            raise errors.BzrCommandError('Start revision not found in'
640
557
                ' history of end revision.')
641
558
 
642
 
    # We exit the loop above because we encounter a revision with merges, from
643
 
    # this revision, we need to switch to _graph_view_revisions.
644
 
 
645
559
    # A log including nested merges is required. If the direction is reverse,
646
560
    # we rebase the initial merge depths so that the development line is
647
561
    # shown naturally, i.e. just like it is for linear logging. We can easily
649
563
    # indented at the end seems slightly nicer in that case.
650
564
    view_revisions = chain(iter(initial_revisions),
651
565
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
652
 
                              rebase_initial_depths=(direction == 'reverse'),
653
 
                              exclude_common_ancestry=exclude_common_ancestry))
654
 
    return view_revisions
 
566
        rebase_initial_depths=direction == 'reverse'))
 
567
    if direction == 'reverse':
 
568
        return view_revisions
 
569
    elif direction == 'forward':
 
570
        # Forward means oldest first, adjusting for depth.
 
571
        view_revisions = reverse_by_depth(list(view_revisions))
 
572
        return _rebase_merge_depth(view_revisions)
 
573
    else:
 
574
        raise ValueError('invalid direction %r' % direction)
655
575
 
656
576
 
657
577
def _has_merges(branch, rev_id):
660
580
    return len(parents) > 1
661
581
 
662
582
 
663
 
def _compute_revno_str(branch, rev_id):
664
 
    """Compute the revno string from a rev_id.
665
 
 
666
 
    :return: The revno string, or None if the revision is not in the supplied
667
 
        branch.
668
 
    """
669
 
    try:
670
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
671
 
    except errors.NoSuchRevision:
672
 
        # The revision must be outside of this branch
673
 
        return None
674
 
    else:
675
 
        return '.'.join(str(n) for n in revno)
676
 
 
677
 
 
678
583
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
679
584
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
680
585
    if start_rev_id and end_rev_id:
681
 
        try:
682
 
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
683
 
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
684
 
        except errors.NoSuchRevision:
685
 
            # one or both is not in the branch; not obvious
686
 
            return False
 
586
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
587
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
687
588
        if len(start_dotted) == 1 and len(end_dotted) == 1:
688
589
            # both on mainline
689
590
            return start_dotted[0] <= end_dotted[0]
694
595
        else:
695
596
            # not obvious
696
597
            return False
697
 
    # if either start or end is not specified then we use either the first or
698
 
    # the last revision and *they* are obvious ancestors.
699
598
    return True
700
599
 
701
600
 
702
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
703
 
                           exclude_common_ancestry=False):
 
601
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
704
602
    """Calculate a sequence of revisions to view, newest to oldest.
705
603
 
706
604
    :param start_rev_id: the lower revision-id
707
605
    :param end_rev_id: the upper revision-id
708
 
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
709
 
        the iterated revisions.
710
606
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
711
607
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
712
 
        is not found walking the left-hand history
 
608
      is not found walking the left-hand history
713
609
    """
714
610
    br_revno, br_rev_id = branch.last_revision_info()
715
611
    repo = branch.repository
716
 
    graph = repo.get_graph()
717
612
    if start_rev_id is None and end_rev_id is None:
718
613
        cur_revno = br_revno
719
 
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
720
 
            (_mod_revision.NULL_REVISION,)):
 
614
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
721
615
            yield revision_id, str(cur_revno), 0
722
616
            cur_revno -= 1
723
617
    else:
724
618
        if end_rev_id is None:
725
619
            end_rev_id = br_rev_id
726
620
        found_start = start_rev_id is None
727
 
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
728
 
                (_mod_revision.NULL_REVISION,)):
729
 
            revno_str = _compute_revno_str(branch, revision_id)
 
621
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
 
622
            revno = branch.revision_id_to_dotted_revno(revision_id)
 
623
            revno_str = '.'.join(str(n) for n in revno)
730
624
            if not found_start and revision_id == start_rev_id:
731
 
                if not exclude_common_ancestry:
732
 
                    yield revision_id, revno_str, 0
 
625
                yield revision_id, revno_str, 0
733
626
                found_start = True
734
627
                break
735
628
            else:
740
633
 
741
634
 
742
635
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
743
 
                          rebase_initial_depths=True,
744
 
                          exclude_common_ancestry=False):
 
636
    rebase_initial_depths=True):
745
637
    """Calculate revisions to view including merges, newest to oldest.
746
638
 
747
639
    :param branch: the branch
751
643
      revision is found?
752
644
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
753
645
    """
754
 
    if exclude_common_ancestry:
755
 
        stop_rule = 'with-merges-without-common-ancestry'
756
 
    else:
757
 
        stop_rule = 'with-merges'
758
646
    view_revisions = branch.iter_merge_sorted_revisions(
759
647
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
760
 
        stop_rule=stop_rule)
 
648
        stop_rule="with-merges")
761
649
    if not rebase_initial_depths:
762
650
        for (rev_id, merge_depth, revno, end_of_merge
763
651
             ) in view_revisions:
774
662
                depth_adjustment = merge_depth
775
663
            if depth_adjustment:
776
664
                if merge_depth < depth_adjustment:
777
 
                    # From now on we reduce the depth adjustement, this can be
778
 
                    # surprising for users. The alternative requires two passes
779
 
                    # which breaks the fast display of the first revision
780
 
                    # though.
781
665
                    depth_adjustment = merge_depth
782
666
                merge_depth -= depth_adjustment
783
667
            yield rev_id, '.'.join(map(str, revno)), merge_depth
784
668
 
785
669
 
786
 
@deprecated_function(deprecated_in((2, 2, 0)))
787
670
def calculate_view_revisions(branch, start_revision, end_revision, direction,
788
671
        specific_fileid, generate_merge_revisions):
789
672
    """Calculate the revisions to view.
791
674
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
792
675
             a list of the same tuples.
793
676
    """
 
677
    # This method is no longer called by the main code path.
 
678
    # It is retained for API compatibility and may be deprecated
 
679
    # soon. IGC 20090116
794
680
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
795
681
        end_revision)
796
682
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
864
750
    """
865
751
    if search is None:
866
752
        return log_rev_iterator
867
 
    searchRE = lazy_regex.lazy_compile(search, re.IGNORECASE)
 
753
    searchRE = re_compile_checked(search, re.IGNORECASE,
 
754
            'log message filter')
868
755
    return _filter_message_re(searchRE, log_rev_iterator)
869
756
 
870
757
 
1124
1011
    cur_revno = branch_revno
1125
1012
    rev_nos = {}
1126
1013
    mainline_revs = []
1127
 
    graph = branch.repository.get_graph()
1128
 
    for revision_id in graph.iter_lefthand_ancestry(
1129
 
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1014
    for revision_id in branch.repository.iter_reverse_revision_history(
 
1015
                        branch_last_revision):
1130
1016
        if cur_revno < start_revno:
1131
1017
            # We have gone far enough, but we always add 1 more revision
1132
1018
            rev_nos[revision_id] = cur_revno
1146
1032
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1147
1033
 
1148
1034
 
1149
 
@deprecated_function(deprecated_in((2, 2, 0)))
1150
1035
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1151
1036
    """Filter view_revisions based on revision ranges.
1152
1037
 
1161
1046
 
1162
1047
    :return: The filtered view_revisions.
1163
1048
    """
 
1049
    # This method is no longer called by the main code path.
 
1050
    # It may be removed soon. IGC 20090127
1164
1051
    if start_rev_id or end_rev_id:
1165
1052
        revision_ids = [r for r, n, d in view_revisions]
1166
1053
        if start_rev_id:
1198
1085
    This includes the revisions which directly change the file id,
1199
1086
    and the revisions which merge these changes. So if the
1200
1087
    revision graph is::
1201
 
 
1202
1088
        A-.
1203
1089
        |\ \
1204
1090
        B C E
1231
1117
    """
1232
1118
    # Lookup all possible text keys to determine which ones actually modified
1233
1119
    # the file.
1234
 
    graph = branch.repository.get_file_graph()
1235
 
    get_parent_map = graph.get_parent_map
1236
1120
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1237
1121
    next_keys = None
1238
1122
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1242
1126
    #       indexing layer. We might consider passing in hints as to the known
1243
1127
    #       access pattern (sparse/clustered, high success rate/low success
1244
1128
    #       rate). This particular access is clustered with a low success rate.
 
1129
    get_parent_map = branch.repository.texts.get_parent_map
1245
1130
    modified_text_revisions = set()
1246
1131
    chunk_size = 1000
1247
1132
    for start in xrange(0, len(text_keys), chunk_size):
1274
1159
    return result
1275
1160
 
1276
1161
 
1277
 
@deprecated_function(deprecated_in((2, 2, 0)))
1278
1162
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1279
1163
                       include_merges=True):
1280
1164
    """Produce an iterator of revisions to show
1281
1165
    :return: an iterator of (revision_id, revno, merge_depth)
1282
1166
    (if there is no revno for a revision, None is supplied)
1283
1167
    """
 
1168
    # This method is no longer called by the main code path.
 
1169
    # It is retained for API compatibility and may be deprecated
 
1170
    # soon. IGC 20090127
1284
1171
    if not include_merges:
1285
1172
        revision_ids = mainline_revs[1:]
1286
1173
        if direction == 'reverse':
1355
1242
    """
1356
1243
 
1357
1244
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1358
 
                 tags=None, diff=None, signature=None):
 
1245
                 tags=None, diff=None):
1359
1246
        self.rev = rev
1360
 
        if revno is None:
1361
 
            self.revno = None
1362
 
        else:
1363
 
            self.revno = str(revno)
 
1247
        self.revno = str(revno)
1364
1248
        self.merge_depth = merge_depth
1365
1249
        self.delta = delta
1366
1250
        self.tags = tags
1367
1251
        self.diff = diff
1368
 
        self.signature = signature
1369
1252
 
1370
1253
 
1371
1254
class LogFormatter(object):
1380
1263
    to indicate which LogRevision attributes it supports:
1381
1264
 
1382
1265
    - supports_delta must be True if this log formatter supports delta.
1383
 
      Otherwise the delta attribute may not be populated.  The 'delta_format'
1384
 
      attribute describes whether the 'short_status' format (1) or the long
1385
 
      one (2) should be used.
 
1266
        Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1267
        attribute describes whether the 'short_status' format (1) or the long
 
1268
        one (2) should be used.
1386
1269
 
1387
1270
    - supports_merge_revisions must be True if this log formatter supports
1388
 
      merge revisions.  If not, then only mainline revisions will be passed
1389
 
      to the formatter.
 
1271
        merge revisions.  If not, then only mainline revisions will be passed
 
1272
        to the formatter.
1390
1273
 
1391
1274
    - preferred_levels is the number of levels this formatter defaults to.
1392
 
      The default value is zero meaning display all levels.
1393
 
      This value is only relevant if supports_merge_revisions is True.
 
1275
        The default value is zero meaning display all levels.
 
1276
        This value is only relevant if supports_merge_revisions is True.
1394
1277
 
1395
1278
    - supports_tags must be True if this log formatter supports tags.
1396
 
      Otherwise the tags attribute may not be populated.
 
1279
        Otherwise the tags attribute may not be populated.
1397
1280
 
1398
1281
    - supports_diff must be True if this log formatter supports diffs.
1399
 
      Otherwise the diff attribute may not be populated.
1400
 
 
1401
 
    - supports_signatures must be True if this log formatter supports GPG
1402
 
      signatures.
 
1282
        Otherwise the diff attribute may not be populated.
1403
1283
 
1404
1284
    Plugins can register functions to show custom revision properties using
1405
1285
    the properties_handler_registry. The registered function
1406
 
    must respect the following interface description::
1407
 
 
 
1286
    must respect the following interface description:
1408
1287
        def my_show_properties(properties_dict):
1409
1288
            # code that returns a dict {'name':'value'} of the properties
1410
1289
            # to be shown
1412
1291
    preferred_levels = 0
1413
1292
 
1414
1293
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1415
 
                 delta_format=None, levels=None, show_advice=False,
1416
 
                 to_exact_file=None, author_list_handler=None):
 
1294
                 delta_format=None, levels=None, show_advice=False):
1417
1295
        """Create a LogFormatter.
1418
1296
 
1419
1297
        :param to_file: the file to output to
1420
 
        :param to_exact_file: if set, gives an output stream to which 
1421
 
             non-Unicode diffs are written.
1422
1298
        :param show_ids: if True, revision-ids are to be displayed
1423
1299
        :param show_timezone: the timezone to use
1424
1300
        :param delta_format: the level of delta information to display
1427
1303
          let the log formatter decide.
1428
1304
        :param show_advice: whether to show advice at the end of the
1429
1305
          log or not
1430
 
        :param author_list_handler: callable generating a list of
1431
 
          authors to display for a given revision
1432
1306
        """
1433
1307
        self.to_file = to_file
1434
1308
        # 'exact' stream used to show diff, it should print content 'as is'
1435
1309
        # and should not try to decode/encode it to unicode to avoid bug #328007
1436
 
        if to_exact_file is not None:
1437
 
            self.to_exact_file = to_exact_file
1438
 
        else:
1439
 
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
1440
 
            # for code that expects to get diffs to pass in the exact file
1441
 
            # stream
1442
 
            self.to_exact_file = getattr(to_file, 'stream', to_file)
 
1310
        self.to_exact_file = getattr(to_file, 'stream', to_file)
1443
1311
        self.show_ids = show_ids
1444
1312
        self.show_timezone = show_timezone
1445
1313
        if delta_format is None:
1449
1317
        self.levels = levels
1450
1318
        self._show_advice = show_advice
1451
1319
        self._merge_count = 0
1452
 
        self._author_list_handler = author_list_handler
1453
1320
 
1454
1321
    def get_levels(self):
1455
1322
        """Get the number of levels to display or 0 for all."""
1487
1354
        return address
1488
1355
 
1489
1356
    def short_author(self, rev):
1490
 
        return self.authors(rev, 'first', short=True, sep=', ')
1491
 
 
1492
 
    def authors(self, rev, who, short=False, sep=None):
1493
 
        """Generate list of authors, taking --authors option into account.
1494
 
 
1495
 
        The caller has to specify the name of a author list handler,
1496
 
        as provided by the author list registry, using the ``who``
1497
 
        argument.  That name only sets a default, though: when the
1498
 
        user selected a different author list generation using the
1499
 
        ``--authors`` command line switch, as represented by the
1500
 
        ``author_list_handler`` constructor argument, that value takes
1501
 
        precedence.
1502
 
 
1503
 
        :param rev: The revision for which to generate the list of authors.
1504
 
        :param who: Name of the default handler.
1505
 
        :param short: Whether to shorten names to either name or address.
1506
 
        :param sep: What separator to use for automatic concatenation.
1507
 
        """
1508
 
        if self._author_list_handler is not None:
1509
 
            # The user did specify --authors, which overrides the default
1510
 
            author_list_handler = self._author_list_handler
1511
 
        else:
1512
 
            # The user didn't specify --authors, so we use the caller's default
1513
 
            author_list_handler = author_list_registry.get(who)
1514
 
        names = author_list_handler(rev)
1515
 
        if short:
1516
 
            for i in range(len(names)):
1517
 
                name, address = config.parse_username(names[i])
1518
 
                if name:
1519
 
                    names[i] = name
1520
 
                else:
1521
 
                    names[i] = address
1522
 
        if sep is not None:
1523
 
            names = sep.join(names)
1524
 
        return names
 
1357
        name, address = config.parse_username(rev.get_apparent_authors()[0])
 
1358
        if name:
 
1359
            return name
 
1360
        return address
1525
1361
 
1526
1362
    def merge_marker(self, revision):
1527
1363
        """Get the merge marker to include in the output or '' if none."""
1531
1367
        else:
1532
1368
            return ''
1533
1369
 
1534
 
    def show_properties(self, revision, indent):
1535
 
        """Displays the custom properties returned by each registered handler.
1536
 
 
1537
 
        If a registered handler raises an error it is propagated.
1538
 
        """
1539
 
        for line in self.custom_properties(revision):
1540
 
            self.to_file.write("%s%s\n" % (indent, line))
1541
 
 
1542
 
    def custom_properties(self, revision):
1543
 
        """Format the custom properties returned by each registered handler.
1544
 
 
1545
 
        If a registered handler raises an error it is propagated.
1546
 
 
1547
 
        :return: a list of formatted lines (excluding trailing newlines)
1548
 
        """
1549
 
        lines = self._foreign_info_properties(revision)
1550
 
        for key, handler in properties_handler_registry.iteritems():
1551
 
            lines.extend(self._format_properties(handler(revision)))
1552
 
        return lines
1553
 
 
1554
 
    def _foreign_info_properties(self, rev):
 
1370
    def show_foreign_info(self, rev, indent):
1555
1371
        """Custom log displayer for foreign revision identifiers.
1556
1372
 
1557
1373
        :param rev: Revision object.
1558
1374
        """
1559
1375
        # Revision comes directly from a foreign repository
1560
1376
        if isinstance(rev, foreign.ForeignRevision):
1561
 
            return self._format_properties(
1562
 
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1377
            self._write_properties(indent, rev.mapping.vcs.show_foreign_revid(
 
1378
                rev.foreign_revid))
 
1379
            return
1563
1380
 
1564
1381
        # Imported foreign revision revision ids always contain :
1565
1382
        if not ":" in rev.revision_id:
1566
 
            return []
 
1383
            return
1567
1384
 
1568
1385
        # Revision was once imported from a foreign repository
1569
1386
        try:
1570
1387
            foreign_revid, mapping = \
1571
1388
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1572
1389
        except errors.InvalidRevisionId:
1573
 
            return []
 
1390
            return
1574
1391
 
1575
 
        return self._format_properties(
 
1392
        self._write_properties(indent, 
1576
1393
            mapping.vcs.show_foreign_revid(foreign_revid))
1577
1394
 
1578
 
    def _format_properties(self, properties):
1579
 
        lines = []
 
1395
    def show_properties(self, revision, indent):
 
1396
        """Displays the custom properties returned by each registered handler.
 
1397
 
 
1398
        If a registered handler raises an error it is propagated.
 
1399
        """
 
1400
        for key, handler in properties_handler_registry.iteritems():
 
1401
            self._write_properties(indent, handler(revision))
 
1402
 
 
1403
    def _write_properties(self, indent, properties):
1580
1404
        for key, value in properties.items():
1581
 
            lines.append(key + ': ' + value)
1582
 
        return lines
 
1405
            self.to_file.write(indent + key + ': ' + value + '\n')
1583
1406
 
1584
1407
    def show_diff(self, to_file, diff, indent):
1585
1408
        for l in diff.rstrip().split('\n'):
1586
1409
            to_file.write(indent + '%s\n' % (l,))
1587
1410
 
1588
1411
 
1589
 
# Separator between revisions in long format
1590
 
_LONG_SEP = '-' * 60
1591
 
 
1592
 
 
1593
1412
class LongLogFormatter(LogFormatter):
1594
1413
 
1595
1414
    supports_merge_revisions = True
1597
1416
    supports_delta = True
1598
1417
    supports_tags = True
1599
1418
    supports_diff = True
1600
 
    supports_signatures = True
1601
 
 
1602
 
    def __init__(self, *args, **kwargs):
1603
 
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1604
 
        if self.show_timezone == 'original':
1605
 
            self.date_string = self._date_string_original_timezone
1606
 
        else:
1607
 
            self.date_string = self._date_string_with_timezone
1608
 
 
1609
 
    def _date_string_with_timezone(self, rev):
1610
 
        return format_date(rev.timestamp, rev.timezone or 0,
1611
 
                           self.show_timezone)
1612
 
 
1613
 
    def _date_string_original_timezone(self, rev):
1614
 
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1615
 
            rev.timezone or 0)
1616
1419
 
1617
1420
    def log_revision(self, revision):
1618
1421
        """Log a revision, either merged or not."""
1619
1422
        indent = '    ' * revision.merge_depth
1620
 
        lines = [_LONG_SEP]
 
1423
        to_file = self.to_file
 
1424
        to_file.write(indent + '-' * 60 + '\n')
1621
1425
        if revision.revno is not None:
1622
 
            lines.append('revno: %s%s' % (revision.revno,
 
1426
            to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
1623
1427
                self.merge_marker(revision)))
1624
1428
        if revision.tags:
1625
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
1626
 
        if self.show_ids or revision.revno is None:
1627
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1429
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1628
1430
        if self.show_ids:
 
1431
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
 
1432
            to_file.write('\n')
1629
1433
            for parent_id in revision.rev.parent_ids:
1630
 
                lines.append('parent: %s' % (parent_id,))
1631
 
        lines.extend(self.custom_properties(revision.rev))
 
1434
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
 
1435
        self.show_foreign_info(revision.rev, indent)
 
1436
        self.show_properties(revision.rev, indent)
1632
1437
 
1633
1438
        committer = revision.rev.committer
1634
 
        authors = self.authors(revision.rev, 'all')
 
1439
        authors = revision.rev.get_apparent_authors()
1635
1440
        if authors != [committer]:
1636
 
            lines.append('author: %s' % (", ".join(authors),))
1637
 
        lines.append('committer: %s' % (committer,))
 
1441
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
 
1442
        to_file.write(indent + 'committer: %s\n' % (committer,))
1638
1443
 
1639
1444
        branch_nick = revision.rev.properties.get('branch-nick', None)
1640
1445
        if branch_nick is not None:
1641
 
            lines.append('branch nick: %s' % (branch_nick,))
1642
 
 
1643
 
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1644
 
 
1645
 
        if revision.signature is not None:
1646
 
            lines.append('signature: ' + revision.signature)
1647
 
 
1648
 
        lines.append('message:')
 
1446
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
 
1447
 
 
1448
        date_str = format_date(revision.rev.timestamp,
 
1449
                               revision.rev.timezone or 0,
 
1450
                               self.show_timezone)
 
1451
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
 
1452
 
 
1453
        to_file.write(indent + 'message:\n')
1649
1454
        if not revision.rev.message:
1650
 
            lines.append('  (no message)')
 
1455
            to_file.write(indent + '  (no message)\n')
1651
1456
        else:
1652
1457
            message = revision.rev.message.rstrip('\r\n')
1653
1458
            for l in message.split('\n'):
1654
 
                lines.append('  %s' % (l,))
1655
 
 
1656
 
        # Dump the output, appending the delta and diff if requested
1657
 
        to_file = self.to_file
1658
 
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
 
1459
                to_file.write(indent + '  %s\n' % (l,))
1659
1460
        if revision.delta is not None:
1660
 
            # Use the standard status output to display changes
1661
 
            from bzrlib.delta import report_delta
1662
 
            report_delta(to_file, revision.delta, short_status=False, 
1663
 
                         show_ids=self.show_ids, indent=indent)
 
1461
            # We don't respect delta_format for compatibility
 
1462
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1463
                                short_status=False)
1664
1464
        if revision.diff is not None:
1665
1465
            to_file.write(indent + 'diff:\n')
1666
 
            to_file.flush()
1667
1466
            # Note: we explicitly don't indent the diff (relative to the
1668
1467
            # revision information) so that the output can be fed to patch -p0
1669
1468
            self.show_diff(self.to_exact_file, revision.diff, indent)
1670
 
            self.to_exact_file.flush()
1671
1469
 
1672
1470
    def get_advice_separator(self):
1673
1471
        """Get the text separating the log from the closing advice."""
1697
1495
        indent = '    ' * depth
1698
1496
        revno_width = self.revno_width_by_depth.get(depth)
1699
1497
        if revno_width is None:
1700
 
            if revision.revno is None or revision.revno.find('.') == -1:
 
1498
            if revision.revno.find('.') == -1:
1701
1499
                # mainline revno, e.g. 12345
1702
1500
                revno_width = 5
1703
1501
            else:
1711
1509
        if revision.tags:
1712
1510
            tags = ' {%s}' % (', '.join(revision.tags))
1713
1511
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1714
 
                revision.revno or "", self.short_author(revision.rev),
 
1512
                revision.revno, self.short_author(revision.rev),
1715
1513
                format_date(revision.rev.timestamp,
1716
1514
                            revision.rev.timezone or 0,
1717
1515
                            self.show_timezone, date_fmt="%Y-%m-%d",
1718
1516
                            show_offset=False),
1719
1517
                tags, self.merge_marker(revision)))
 
1518
        self.show_foreign_info(revision.rev, indent+offset)
1720
1519
        self.show_properties(revision.rev, indent+offset)
1721
 
        if self.show_ids or revision.revno is None:
 
1520
        if self.show_ids:
1722
1521
            to_file.write(indent + offset + 'revision-id:%s\n'
1723
1522
                          % (revision.rev.revision_id,))
1724
1523
        if not revision.rev.message:
1729
1528
                to_file.write(indent + offset + '%s\n' % (l,))
1730
1529
 
1731
1530
        if revision.delta is not None:
1732
 
            # Use the standard status output to display changes
1733
 
            from bzrlib.delta import report_delta
1734
 
            report_delta(to_file, revision.delta, 
1735
 
                         short_status=self.delta_format==1, 
1736
 
                         show_ids=self.show_ids, indent=indent + offset)
 
1531
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1532
                                short_status=self.delta_format==1)
1737
1533
        if revision.diff is not None:
1738
1534
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1739
1535
        to_file.write('\n')
1747
1543
 
1748
1544
    def __init__(self, *args, **kwargs):
1749
1545
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1750
 
        width = terminal_width()
1751
 
        if width is not None:
1752
 
            # we need one extra space for terminals that wrap on last char
1753
 
            width = width - 1
1754
 
        self._max_chars = width
 
1546
        self._max_chars = terminal_width() - 1
1755
1547
 
1756
1548
    def truncate(self, str, max_len):
1757
 
        if max_len is None or len(str) <= max_len:
 
1549
        if len(str) <= max_len:
1758
1550
            return str
1759
 
        return str[:max_len-3] + '...'
 
1551
        return str[:max_len-3]+'...'
1760
1552
 
1761
1553
    def date_string(self, rev):
1762
1554
        return format_date(rev.timestamp, rev.timezone or 0,
1777
1569
 
1778
1570
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1779
1571
        """Format log info into one string. Truncate tail of string
1780
 
 
1781
 
        :param revno:      revision number or None.
1782
 
                           Revision numbers counts from 1.
1783
 
        :param rev:        revision object
1784
 
        :param max_chars:  maximum length of resulting string
1785
 
        :param tags:       list of tags or None
1786
 
        :param prefix:     string to prefix each line
1787
 
        :return:           formatted truncated string
 
1572
        :param  revno:      revision number or None.
 
1573
                            Revision numbers counts from 1.
 
1574
        :param  rev:        revision object
 
1575
        :param  max_chars:  maximum length of resulting string
 
1576
        :param  tags:       list of tags or None
 
1577
        :param  prefix:     string to prefix each line
 
1578
        :return:            formatted truncated string
1788
1579
        """
1789
1580
        out = []
1790
1581
        if revno:
1791
1582
            # show revno only when is not None
1792
1583
            out.append("%s:" % revno)
1793
 
        if max_chars is not None:
1794
 
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1795
 
        else:
1796
 
            out.append(self.short_author(rev))
 
1584
        out.append(self.truncate(self.short_author(rev), 20))
1797
1585
        out.append(self.date_string(rev))
1798
1586
        if len(rev.parent_ids) > 1:
1799
1587
            out.append('[merge]')
1818
1606
                               self.show_timezone,
1819
1607
                               date_fmt='%Y-%m-%d',
1820
1608
                               show_offset=False)
1821
 
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1822
 
        committer_str = committer_str.replace(' <', '  <')
 
1609
        committer_str = revision.rev.committer.replace (' <', '  <')
1823
1610
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1824
1611
 
1825
1612
        if revision.delta is not None and revision.delta.has_changed():
1890
1677
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1891
1678
 
1892
1679
 
1893
 
def author_list_all(rev):
1894
 
    return rev.get_apparent_authors()[:]
1895
 
 
1896
 
 
1897
 
def author_list_first(rev):
1898
 
    lst = rev.get_apparent_authors()
1899
 
    try:
1900
 
        return [lst[0]]
1901
 
    except IndexError:
1902
 
        return []
1903
 
 
1904
 
 
1905
 
def author_list_committer(rev):
1906
 
    return [rev.committer]
1907
 
 
1908
 
 
1909
 
author_list_registry = registry.Registry()
1910
 
 
1911
 
author_list_registry.register('all', author_list_all,
1912
 
                              'All authors')
1913
 
 
1914
 
author_list_registry.register('first', author_list_first,
1915
 
                              'The first author')
1916
 
 
1917
 
author_list_registry.register('committer', author_list_committer,
1918
 
                              'The committer')
1919
 
 
1920
 
 
1921
1680
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1922
1681
    # deprecated; for compatibility
1923
1682
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1992
1751
    old_revisions = set()
1993
1752
    new_history = []
1994
1753
    new_revisions = set()
1995
 
    graph = repository.get_graph()
1996
 
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
1997
 
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
 
1754
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
 
1755
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
1998
1756
    stop_revision = None
1999
1757
    do_old = True
2000
1758
    do_new = True
2075
1833
        lf.log_revision(lr)
2076
1834
 
2077
1835
 
2078
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
1836
def _get_info_for_log_files(revisionspec_list, file_list):
2079
1837
    """Find file-ids and kinds given a list of files and a revision range.
2080
1838
 
2081
1839
    We search for files at the end of the range. If not found there,
2085
1843
    :param file_list: the list of paths given on the command line;
2086
1844
      the first of these can be a branch location or a file path,
2087
1845
      the remainder must be file paths
2088
 
    :param add_cleanup: When the branch returned is read locked,
2089
 
      an unlock call will be queued to the cleanup.
2090
1846
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2091
1847
      info_list is a list of (relative_path, file_id, kind) tuples where
2092
1848
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2093
 
      branch will be read-locked.
2094
1849
    """
2095
 
    from builtins import _get_revision_range
 
1850
    from builtins import _get_revision_range, safe_relpath_files
2096
1851
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2097
 
    add_cleanup(b.lock_read().unlock)
2098
1852
    # XXX: It's damn messy converting a list of paths to relative paths when
2099
1853
    # those paths might be deleted ones, they might be on a case-insensitive
2100
1854
    # filesystem and/or they might be in silly locations (like another branch).
2104
1858
    # case of running log in a nested directory, assuming paths beyond the
2105
1859
    # first one haven't been deleted ...
2106
1860
    if tree:
2107
 
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
1861
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2108
1862
    else:
2109
1863
        relpaths = [path] + file_list[1:]
2110
1864
    info_list = []
2179
1933
 
2180
1934
properties_handler_registry = registry.Registry()
2181
1935
 
2182
 
# Use the properties handlers to print out bug information if available
2183
 
def _bugs_properties_handler(revision):
2184
 
    if revision.properties.has_key('bugs'):
2185
 
        bug_lines = revision.properties['bugs'].split('\n')
2186
 
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2187
 
        fixed_bug_urls = [row[0] for row in bug_rows if
2188
 
                          len(row) > 1 and row[1] == 'fixed']
2189
 
 
2190
 
        if fixed_bug_urls:
2191
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2192
 
    return {}
2193
 
 
2194
 
properties_handler_registry.register('bugs_properties_handler',
2195
 
                                     _bugs_properties_handler)
2196
 
 
2197
1936
 
2198
1937
# adapters which revision ids to log are filtered. When log is called, the
2199
1938
# log_rev_iterator is adapted through each of these factory methods.