~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Vincent Ladeuil
  • Date: 2011-09-09 13:30:12 UTC
  • mfrom: (5609.48.11 2.3)
  • mto: (6015.33.3 2.4)
  • mto: This revision was merged to the branch mainline in revision 6134.
  • Revision ID: v.ladeuil+lp@free.fr-20110909133012-jc1d1zyqgak57123
Merge 2.3 into 2.4

Show diffs side-by-side

added added

removed removed

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