~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: John Arbash Meinel
  • Date: 2009-03-27 22:29:55 UTC
  • mto: (3735.39.2 clean)
  • mto: This revision was merged to the branch mainline in revision 4280.
  • Revision ID: john@arbash-meinel.com-20090327222955-utifmfm888zerixt
Implement apply_delta_to_source which doesn't have to malloc another string.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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
69
69
    config,
70
70
    diff,
71
71
    errors,
72
 
    foreign,
73
 
    osutils,
74
72
    repository as _mod_repository,
75
73
    revision as _mod_revision,
76
74
    revisionspec,
84
82
    )
85
83
from bzrlib.osutils import (
86
84
    format_date,
87
 
    format_date_with_offset_in_original_timezone,
88
85
    get_terminal_encoding,
 
86
    re_compile_checked,
89
87
    terminal_width,
90
88
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
94
 
    )
95
89
 
96
90
 
97
91
def find_touching_revisions(branch, file_id):
109
103
    last_path = None
110
104
    revno = 1
111
105
    for revision_id in branch.revision_history():
112
 
        this_inv = branch.repository.get_inventory(revision_id)
 
106
        this_inv = branch.repository.get_revision_inventory(revision_id)
113
107
        if file_id in this_inv:
114
108
            this_ie = this_inv[file_id]
115
109
            this_path = this_inv.id2path(file_id)
220
214
    'direction': 'reverse',
221
215
    'levels': 1,
222
216
    'generate_tags': True,
223
 
    'exclude_common_ancestry': False,
224
217
    '_match_using_deltas': True,
225
218
    }
226
219
 
227
220
 
228
221
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
 
                          start_revision=None, end_revision=None, limit=None,
230
 
                          message_search=None, levels=1, generate_tags=True,
231
 
                          delta_type=None,
232
 
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
234
 
                          ):
 
222
    start_revision=None, end_revision=None, limit=None,
 
223
    message_search=None, levels=1, generate_tags=True, delta_type=None,
 
224
    diff_type=None, _match_using_deltas=True):
235
225
    """Convenience function for making a logging request dictionary.
236
226
 
237
227
    Using this function may make code slightly safer by ensuring
275
265
      algorithm used for matching specific_fileids. This parameter
276
266
      may be removed in the future so bzrlib client code should NOT
277
267
      use it.
278
 
 
279
 
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
 
      range operator or as a graph difference.
281
268
    """
282
269
    return {
283
270
        'direction': direction,
290
277
        'generate_tags': generate_tags,
291
278
        'delta_type': delta_type,
292
279
        'diff_type': diff_type,
293
 
        'exclude_common_ancestry': exclude_common_ancestry,
294
280
        # Add 'private' attributes for features that may be deprecated
295
281
        '_match_using_deltas': _match_using_deltas,
 
282
        '_allow_single_merge_revision': True,
296
283
    }
297
284
 
298
285
 
316
303
 
317
304
 
318
305
class Logger(object):
319
 
    """An object that generates, formats and displays a log."""
 
306
    """An object the generates, formats and displays a log."""
320
307
 
321
308
    def __init__(self, branch, rqst):
322
309
        """Create a Logger.
361
348
            rqst['delta_type'] = None
362
349
        if not getattr(lf, 'supports_diff', False):
363
350
            rqst['diff_type'] = None
 
351
        if not getattr(lf, 'supports_merge_revisions', False):
 
352
            rqst['_allow_single_merge_revision'] = getattr(lf,
 
353
                'supports_single_merge_revision', False)
364
354
 
365
355
        # Find and print the interesting revisions
366
356
        generator = self._generator_factory(self.branch, rqst)
367
357
        for lr in generator.iter_log_revisions():
368
358
            lf.log_revision(lr)
369
 
        lf.show_advice()
370
359
 
371
360
    def _generator_factory(self, branch, rqst):
372
361
        """Make the LogGenerator object to use.
397
386
        :return: An iterator yielding LogRevision objects.
398
387
        """
399
388
        rqst = self.rqst
400
 
        levels = rqst.get('levels')
401
 
        limit = rqst.get('limit')
402
 
        diff_type = rqst.get('diff_type')
403
389
        log_count = 0
404
390
        revision_iterator = self._create_log_revision_iterator()
405
391
        for revs in revision_iterator:
406
392
            for (rev_id, revno, merge_depth), rev, delta in revs:
407
393
                # 0 levels means show everything; merge_depth counts from 0
 
394
                levels = rqst.get('levels')
408
395
                if levels != 0 and merge_depth >= levels:
409
396
                    continue
410
 
                if diff_type is None:
411
 
                    diff = None
412
 
                else:
413
 
                    diff = self._format_diff(rev, rev_id, diff_type)
 
397
                diff = self._format_diff(rev, rev_id)
414
398
                yield LogRevision(rev, revno, merge_depth, delta,
415
399
                    self.rev_tag_dict.get(rev_id), diff)
 
400
                limit = rqst.get('limit')
416
401
                if limit:
417
402
                    log_count += 1
418
403
                    if log_count >= limit:
419
404
                        return
420
405
 
421
 
    def _format_diff(self, rev, rev_id, diff_type):
 
406
    def _format_diff(self, rev, rev_id):
 
407
        diff_type = self.rqst.get('diff_type')
 
408
        if diff_type is None:
 
409
            return None
422
410
        repo = self.branch.repository
423
411
        if len(rev.parent_ids) == 0:
424
412
            ancestor_id = _mod_revision.NULL_REVISION
432
420
        else:
433
421
            specific_files = None
434
422
        s = StringIO()
435
 
        path_encoding = osutils.get_diff_header_encoding()
436
423
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
437
 
            new_label='', path_encoding=path_encoding)
 
424
            new_label='')
438
425
        return s.getvalue()
439
426
 
440
427
    def _create_log_revision_iterator(self):
464
451
        generate_merge_revisions = rqst.get('levels') != 1
465
452
        delayed_graph_generation = not rqst.get('specific_fileids') and (
466
453
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
467
 
        view_revisions = _calc_view_revisions(
468
 
            self.branch, self.start_rev_id, self.end_rev_id,
469
 
            rqst.get('direction'),
470
 
            generate_merge_revisions=generate_merge_revisions,
471
 
            delayed_graph_generation=delayed_graph_generation,
472
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
454
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
455
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
 
456
            rqst.get('_allow_single_merge_revision'),
 
457
            delayed_graph_generation=delayed_graph_generation)
473
458
 
474
459
        # Apply the other filters
475
460
        return make_log_rev_iterator(self.branch, view_revisions,
482
467
        # Note that we always generate the merge revisions because
483
468
        # filter_revisions_touching_file_id() requires them ...
484
469
        rqst = self.rqst
485
 
        view_revisions = _calc_view_revisions(
486
 
            self.branch, self.start_rev_id, self.end_rev_id,
487
 
            rqst.get('direction'), generate_merge_revisions=True,
488
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
470
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
471
            self.end_rev_id, rqst.get('direction'), True,
 
472
            rqst.get('_allow_single_merge_revision'))
489
473
        if not isinstance(view_revisions, list):
490
474
            view_revisions = list(view_revisions)
491
475
        view_revisions = _filter_revisions_touching_file_id(self.branch,
496
480
 
497
481
 
498
482
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
499
 
                         generate_merge_revisions,
500
 
                         delayed_graph_generation=False,
501
 
                         exclude_common_ancestry=False,
502
 
                         ):
 
483
    generate_merge_revisions, allow_single_merge_revision,
 
484
    delayed_graph_generation=False):
503
485
    """Calculate the revisions to view.
504
486
 
505
487
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
506
488
             a list of the same tuples.
507
489
    """
508
 
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
509
 
        raise errors.BzrCommandError(
510
 
            '--exclude-common-ancestry requires two different revisions')
511
 
    if direction not in ('reverse', 'forward'):
512
 
        raise ValueError('invalid direction %r' % direction)
513
490
    br_revno, br_rev_id = branch.last_revision_info()
514
491
    if br_revno == 0:
515
492
        return []
516
493
 
517
 
    if (end_rev_id and start_rev_id == end_rev_id
518
 
        and (not generate_merge_revisions
519
 
             or not _has_merges(branch, end_rev_id))):
520
 
        # If a single revision is requested, check we can handle it
521
 
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
522
 
                                           br_revno)
523
 
    elif not generate_merge_revisions:
524
 
        # If we only want to see linear revisions, we can iterate ...
525
 
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
526
 
                                             direction, exclude_common_ancestry)
527
 
        if direction == 'forward':
528
 
            iter_revs = reversed(iter_revs)
 
494
    # If a single revision is requested, check we can handle it
 
495
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
 
496
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
 
497
    if generate_single_revision:
 
498
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno,
 
499
            allow_single_merge_revision)
 
500
 
 
501
    # If we only want to see linear revisions, we can iterate ...
 
502
    if not generate_merge_revisions:
 
503
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
504
            direction)
529
505
    else:
530
 
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
531
 
                                            direction, delayed_graph_generation,
532
 
                                            exclude_common_ancestry)
533
 
        if direction == 'forward':
534
 
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
535
 
    return iter_revs
536
 
 
537
 
 
538
 
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
 
506
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
507
            direction, delayed_graph_generation)
 
508
 
 
509
 
 
510
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno,
 
511
    allow_single_merge_revision):
539
512
    if rev_id == br_rev_id:
540
513
        # It's the tip
541
514
        return [(br_rev_id, br_revno, 0)]
542
515
    else:
543
516
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
517
        if len(revno) > 1 and not allow_single_merge_revision:
 
518
            # It's a merge revision and the log formatter is
 
519
            # completely brain dead. This "feature" of allowing
 
520
            # log formatters incapable of displaying dotted revnos
 
521
            # ought to be deprecated IMNSHO. IGC 20091022
 
522
            raise errors.BzrCommandError('Selected log formatter only'
 
523
                ' supports mainline revisions.')
544
524
        revno_str = '.'.join(str(n) for n in revno)
545
525
        return [(rev_id, revno_str, 0)]
546
526
 
547
527
 
548
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
549
 
                             exclude_common_ancestry=False):
550
 
    result = _linear_view_revisions(
551
 
        branch, start_rev_id, end_rev_id,
552
 
        exclude_common_ancestry=exclude_common_ancestry)
 
528
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
 
529
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
553
530
    # If a start limit was given and it's not obviously an
554
531
    # ancestor of the end limit, check it before outputting anything
555
532
    if direction == 'forward' or (start_rev_id
559
536
        except _StartNotLinearAncestor:
560
537
            raise errors.BzrCommandError('Start revision not found in'
561
538
                ' left-hand history of end revision.')
 
539
    if direction == 'forward':
 
540
        result = reversed(result)
562
541
    return result
563
542
 
564
543
 
565
544
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
566
 
                            delayed_graph_generation,
567
 
                            exclude_common_ancestry=False):
 
545
    delayed_graph_generation):
568
546
    # On large trees, generating the merge graph can take 30-60 seconds
569
547
    # so we delay doing it until a merge is detected, incrementally
570
548
    # returning initial (non-merge) revisions while we can.
571
 
 
572
 
    # The above is only true for old formats (<= 0.92), for newer formats, a
573
 
    # couple of seconds only should be needed to load the whole graph and the
574
 
    # other graph operations needed are even faster than that -- vila 100201
575
549
    initial_revisions = []
576
550
    if delayed_graph_generation:
577
551
        try:
578
 
            for rev_id, revno, depth in  _linear_view_revisions(
579
 
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
552
            for rev_id, revno, depth in \
 
553
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
580
554
                if _has_merges(branch, rev_id):
581
 
                    # The end_rev_id can be nested down somewhere. We need an
582
 
                    # explicit ancestry check. There is an ambiguity here as we
583
 
                    # may not raise _StartNotLinearAncestor for a revision that
584
 
                    # is an ancestor but not a *linear* one. But since we have
585
 
                    # loaded the graph to do the check (or calculate a dotted
586
 
                    # revno), we may as well accept to show the log...  We need
587
 
                    # the check only if start_rev_id is not None as all
588
 
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
589
 
                    # -- vila 20100319
590
 
                    graph = branch.repository.get_graph()
591
 
                    if (start_rev_id is not None
592
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
593
 
                        raise _StartNotLinearAncestor()
594
 
                    # Since we collected the revisions so far, we need to
595
 
                    # adjust end_rev_id.
596
555
                    end_rev_id = rev_id
597
556
                    break
598
557
                else:
599
558
                    initial_revisions.append((rev_id, revno, depth))
600
559
            else:
601
560
                # No merged revisions found
602
 
                return initial_revisions
 
561
                if direction == 'reverse':
 
562
                    return initial_revisions
 
563
                elif direction == 'forward':
 
564
                    return reversed(initial_revisions)
 
565
                else:
 
566
                    raise ValueError('invalid direction %r' % direction)
603
567
        except _StartNotLinearAncestor:
604
568
            # A merge was never detected so the lower revision limit can't
605
569
            # be nested down somewhere
606
570
            raise errors.BzrCommandError('Start revision not found in'
607
571
                ' history of end revision.')
608
572
 
609
 
    # We exit the loop above because we encounter a revision with merges, from
610
 
    # this revision, we need to switch to _graph_view_revisions.
611
 
 
612
573
    # A log including nested merges is required. If the direction is reverse,
613
574
    # we rebase the initial merge depths so that the development line is
614
575
    # shown naturally, i.e. just like it is for linear logging. We can easily
616
577
    # indented at the end seems slightly nicer in that case.
617
578
    view_revisions = chain(iter(initial_revisions),
618
579
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
619
 
                              rebase_initial_depths=(direction == 'reverse'),
620
 
                              exclude_common_ancestry=exclude_common_ancestry))
621
 
    return view_revisions
 
580
        rebase_initial_depths=direction == 'reverse'))
 
581
    if direction == 'reverse':
 
582
        return view_revisions
 
583
    elif direction == 'forward':
 
584
        # Forward means oldest first, adjusting for depth.
 
585
        view_revisions = reverse_by_depth(list(view_revisions))
 
586
        return _rebase_merge_depth(view_revisions)
 
587
    else:
 
588
        raise ValueError('invalid direction %r' % direction)
622
589
 
623
590
 
624
591
def _has_merges(branch, rev_id):
642
609
        else:
643
610
            # not obvious
644
611
            return False
645
 
    # if either start or end is not specified then we use either the first or
646
 
    # the last revision and *they* are obvious ancestors.
647
612
    return True
648
613
 
649
614
 
650
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
651
 
                           exclude_common_ancestry=False):
 
615
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
652
616
    """Calculate a sequence of revisions to view, newest to oldest.
653
617
 
654
618
    :param start_rev_id: the lower revision-id
655
619
    :param end_rev_id: the upper revision-id
656
 
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
657
 
        the iterated revisions.
658
620
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
659
621
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
660
 
        is not found walking the left-hand history
 
622
      is not found walking the left-hand history
661
623
    """
662
624
    br_revno, br_rev_id = branch.last_revision_info()
663
625
    repo = branch.repository
674
636
            revno = branch.revision_id_to_dotted_revno(revision_id)
675
637
            revno_str = '.'.join(str(n) for n in revno)
676
638
            if not found_start and revision_id == start_rev_id:
677
 
                if not exclude_common_ancestry:
678
 
                    yield revision_id, revno_str, 0
 
639
                yield revision_id, revno_str, 0
679
640
                found_start = True
680
641
                break
681
642
            else:
686
647
 
687
648
 
688
649
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
689
 
                          rebase_initial_depths=True,
690
 
                          exclude_common_ancestry=False):
 
650
    rebase_initial_depths=True):
691
651
    """Calculate revisions to view including merges, newest to oldest.
692
652
 
693
653
    :param branch: the branch
697
657
      revision is found?
698
658
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
699
659
    """
700
 
    if exclude_common_ancestry:
701
 
        stop_rule = 'with-merges-without-common-ancestry'
702
 
    else:
703
 
        stop_rule = 'with-merges'
704
660
    view_revisions = branch.iter_merge_sorted_revisions(
705
661
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
706
 
        stop_rule=stop_rule)
 
662
        stop_rule="with-merges")
707
663
    if not rebase_initial_depths:
708
664
        for (rev_id, merge_depth, revno, end_of_merge
709
665
             ) in view_revisions:
720
676
                depth_adjustment = merge_depth
721
677
            if depth_adjustment:
722
678
                if merge_depth < depth_adjustment:
723
 
                    # From now on we reduce the depth adjustement, this can be
724
 
                    # surprising for users. The alternative requires two passes
725
 
                    # which breaks the fast display of the first revision
726
 
                    # though.
727
679
                    depth_adjustment = merge_depth
728
680
                merge_depth -= depth_adjustment
729
681
            yield rev_id, '.'.join(map(str, revno)), merge_depth
730
682
 
731
683
 
732
 
@deprecated_function(deprecated_in((2, 2, 0)))
733
684
def calculate_view_revisions(branch, start_revision, end_revision, direction,
734
 
        specific_fileid, generate_merge_revisions):
 
685
        specific_fileid, generate_merge_revisions, allow_single_merge_revision):
735
686
    """Calculate the revisions to view.
736
687
 
737
688
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
738
689
             a list of the same tuples.
739
690
    """
 
691
    # This method is no longer called by the main code path.
 
692
    # It is retained for API compatibility and may be deprecated
 
693
    # soon. IGC 20090116
740
694
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
741
695
        end_revision)
742
696
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
743
 
        direction, generate_merge_revisions or specific_fileid))
 
697
        direction, generate_merge_revisions or specific_fileid,
 
698
        allow_single_merge_revision))
744
699
    if specific_fileid:
745
700
        view_revisions = _filter_revisions_touching_file_id(branch,
746
701
            specific_fileid, view_revisions,
810
765
    """
811
766
    if search is None:
812
767
        return log_rev_iterator
813
 
    searchRE = re.compile(search, re.IGNORECASE)
 
768
    searchRE = re_compile_checked(search, re.IGNORECASE,
 
769
            'log message filter')
814
770
    return _filter_message_re(searchRE, log_rev_iterator)
815
771
 
816
772
 
1091
1047
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1092
1048
 
1093
1049
 
1094
 
@deprecated_function(deprecated_in((2, 2, 0)))
1095
1050
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1096
1051
    """Filter view_revisions based on revision ranges.
1097
1052
 
1106
1061
 
1107
1062
    :return: The filtered view_revisions.
1108
1063
    """
 
1064
    # This method is no longer called by the main code path.
 
1065
    # It may be removed soon. IGC 20090127
1109
1066
    if start_rev_id or end_rev_id:
1110
1067
        revision_ids = [r for r, n, d in view_revisions]
1111
1068
        if start_rev_id:
1217
1174
    return result
1218
1175
 
1219
1176
 
1220
 
@deprecated_function(deprecated_in((2, 2, 0)))
1221
1177
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1222
1178
                       include_merges=True):
1223
1179
    """Produce an iterator of revisions to show
1224
1180
    :return: an iterator of (revision_id, revno, merge_depth)
1225
1181
    (if there is no revno for a revision, None is supplied)
1226
1182
    """
 
1183
    # This method is no longer called by the main code path.
 
1184
    # It is retained for API compatibility and may be deprecated
 
1185
    # soon. IGC 20090127
1227
1186
    if not include_merges:
1228
1187
        revision_ids = mainline_revs[1:]
1229
1188
        if direction == 'reverse':
1324
1283
        one (2) should be used.
1325
1284
 
1326
1285
    - supports_merge_revisions must be True if this log formatter supports
1327
 
        merge revisions.  If not, then only mainline revisions will be passed
1328
 
        to the formatter.
 
1286
        merge revisions.  If not, and if supports_single_merge_revision is
 
1287
        also not True, then only mainline revisions will be passed to the
 
1288
        formatter.
1329
1289
 
1330
1290
    - preferred_levels is the number of levels this formatter defaults to.
1331
1291
        The default value is zero meaning display all levels.
1332
1292
        This value is only relevant if supports_merge_revisions is True.
1333
1293
 
 
1294
    - supports_single_merge_revision must be True if this log formatter
 
1295
        supports logging only a single merge revision.  This flag is
 
1296
        only relevant if supports_merge_revisions is not True.
 
1297
 
1334
1298
    - supports_tags must be True if this log formatter supports tags.
1335
1299
        Otherwise the tags attribute may not be populated.
1336
1300
 
1347
1311
    preferred_levels = 0
1348
1312
 
1349
1313
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1350
 
                 delta_format=None, levels=None, show_advice=False,
1351
 
                 to_exact_file=None, author_list_handler=None):
 
1314
                 delta_format=None, levels=None):
1352
1315
        """Create a LogFormatter.
1353
1316
 
1354
1317
        :param to_file: the file to output to
1355
 
        :param to_exact_file: if set, gives an output stream to which 
1356
 
             non-Unicode diffs are written.
1357
1318
        :param show_ids: if True, revision-ids are to be displayed
1358
1319
        :param show_timezone: the timezone to use
1359
1320
        :param delta_format: the level of delta information to display
1360
 
          or None to leave it to the formatter to decide
 
1321
          or None to leave it u to the formatter to decide
1361
1322
        :param levels: the number of levels to display; None or -1 to
1362
1323
          let the log formatter decide.
1363
 
        :param show_advice: whether to show advice at the end of the
1364
 
          log or not
1365
 
        :param author_list_handler: callable generating a list of
1366
 
          authors to display for a given revision
1367
1324
        """
1368
1325
        self.to_file = to_file
1369
1326
        # 'exact' stream used to show diff, it should print content 'as is'
1370
1327
        # and should not try to decode/encode it to unicode to avoid bug #328007
1371
 
        if to_exact_file is not None:
1372
 
            self.to_exact_file = to_exact_file
1373
 
        else:
1374
 
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
1375
 
            # for code that expects to get diffs to pass in the exact file
1376
 
            # stream
1377
 
            self.to_exact_file = getattr(to_file, 'stream', to_file)
 
1328
        self.to_exact_file = getattr(to_file, 'stream', to_file)
1378
1329
        self.show_ids = show_ids
1379
1330
        self.show_timezone = show_timezone
1380
1331
        if delta_format is None:
1382
1333
            delta_format = 2 # long format
1383
1334
        self.delta_format = delta_format
1384
1335
        self.levels = levels
1385
 
        self._show_advice = show_advice
1386
 
        self._merge_count = 0
1387
 
        self._author_list_handler = author_list_handler
1388
1336
 
1389
1337
    def get_levels(self):
1390
1338
        """Get the number of levels to display or 0 for all."""
1391
1339
        if getattr(self, 'supports_merge_revisions', False):
1392
1340
            if self.levels is None or self.levels == -1:
1393
 
                self.levels = self.preferred_levels
1394
 
        else:
1395
 
            self.levels = 1
1396
 
        return self.levels
 
1341
                return self.preferred_levels
 
1342
            else:
 
1343
                return self.levels
 
1344
        return 1
1397
1345
 
1398
1346
    def log_revision(self, revision):
1399
1347
        """Log a revision.
1402
1350
        """
1403
1351
        raise NotImplementedError('not implemented in abstract base')
1404
1352
 
1405
 
    def show_advice(self):
1406
 
        """Output user advice, if any, when the log is completed."""
1407
 
        if self._show_advice and self.levels == 1 and self._merge_count > 0:
1408
 
            advice_sep = self.get_advice_separator()
1409
 
            if advice_sep:
1410
 
                self.to_file.write(advice_sep)
1411
 
            self.to_file.write(
1412
 
                "Use --include-merges or -n0 to see merged revisions.\n")
1413
 
 
1414
 
    def get_advice_separator(self):
1415
 
        """Get the text separating the log from the closing advice."""
1416
 
        return ''
1417
 
 
1418
1353
    def short_committer(self, rev):
1419
1354
        name, address = config.parse_username(rev.committer)
1420
1355
        if name:
1422
1357
        return address
1423
1358
 
1424
1359
    def short_author(self, rev):
1425
 
        return self.authors(rev, 'first', short=True, sep=', ')
1426
 
 
1427
 
    def authors(self, rev, who, short=False, sep=None):
1428
 
        """Generate list of authors, taking --authors option into account.
1429
 
 
1430
 
        The caller has to specify the name of a author list handler,
1431
 
        as provided by the author list registry, using the ``who``
1432
 
        argument.  That name only sets a default, though: when the
1433
 
        user selected a different author list generation using the
1434
 
        ``--authors`` command line switch, as represented by the
1435
 
        ``author_list_handler`` constructor argument, that value takes
1436
 
        precedence.
1437
 
 
1438
 
        :param rev: The revision for which to generate the list of authors.
1439
 
        :param who: Name of the default handler.
1440
 
        :param short: Whether to shorten names to either name or address.
1441
 
        :param sep: What separator to use for automatic concatenation.
1442
 
        """
1443
 
        if self._author_list_handler is not None:
1444
 
            # The user did specify --authors, which overrides the default
1445
 
            author_list_handler = self._author_list_handler
1446
 
        else:
1447
 
            # The user didn't specify --authors, so we use the caller's default
1448
 
            author_list_handler = author_list_registry.get(who)
1449
 
        names = author_list_handler(rev)
1450
 
        if short:
1451
 
            for i in range(len(names)):
1452
 
                name, address = config.parse_username(names[i])
1453
 
                if name:
1454
 
                    names[i] = name
1455
 
                else:
1456
 
                    names[i] = address
1457
 
        if sep is not None:
1458
 
            names = sep.join(names)
1459
 
        return names
1460
 
 
1461
 
    def merge_marker(self, revision):
1462
 
        """Get the merge marker to include in the output or '' if none."""
1463
 
        if len(revision.rev.parent_ids) > 1:
1464
 
            self._merge_count += 1
1465
 
            return ' [merge]'
1466
 
        else:
1467
 
            return ''
 
1360
        name, address = config.parse_username(rev.get_apparent_authors()[0])
 
1361
        if name:
 
1362
            return name
 
1363
        return address
1468
1364
 
1469
1365
    def show_properties(self, revision, indent):
1470
1366
        """Displays the custom properties returned by each registered handler.
1471
1367
 
1472
1368
        If a registered handler raises an error it is propagated.
1473
1369
        """
1474
 
        for line in self.custom_properties(revision):
1475
 
            self.to_file.write("%s%s\n" % (indent, line))
1476
 
 
1477
 
    def custom_properties(self, revision):
1478
 
        """Format the custom properties returned by each registered handler.
1479
 
 
1480
 
        If a registered handler raises an error it is propagated.
1481
 
 
1482
 
        :return: a list of formatted lines (excluding trailing newlines)
1483
 
        """
1484
 
        lines = self._foreign_info_properties(revision)
1485
1370
        for key, handler in properties_handler_registry.iteritems():
1486
 
            lines.extend(self._format_properties(handler(revision)))
1487
 
        return lines
1488
 
 
1489
 
    def _foreign_info_properties(self, rev):
1490
 
        """Custom log displayer for foreign revision identifiers.
1491
 
 
1492
 
        :param rev: Revision object.
1493
 
        """
1494
 
        # Revision comes directly from a foreign repository
1495
 
        if isinstance(rev, foreign.ForeignRevision):
1496
 
            return self._format_properties(
1497
 
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1498
 
 
1499
 
        # Imported foreign revision revision ids always contain :
1500
 
        if not ":" in rev.revision_id:
1501
 
            return []
1502
 
 
1503
 
        # Revision was once imported from a foreign repository
1504
 
        try:
1505
 
            foreign_revid, mapping = \
1506
 
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1507
 
        except errors.InvalidRevisionId:
1508
 
            return []
1509
 
 
1510
 
        return self._format_properties(
1511
 
            mapping.vcs.show_foreign_revid(foreign_revid))
1512
 
 
1513
 
    def _format_properties(self, properties):
1514
 
        lines = []
1515
 
        for key, value in properties.items():
1516
 
            lines.append(key + ': ' + value)
1517
 
        return lines
 
1371
            for key, value in handler(revision).items():
 
1372
                self.to_file.write(indent + key + ': ' + value + '\n')
1518
1373
 
1519
1374
    def show_diff(self, to_file, diff, indent):
1520
1375
        for l in diff.rstrip().split('\n'):
1521
1376
            to_file.write(indent + '%s\n' % (l,))
1522
1377
 
1523
1378
 
1524
 
# Separator between revisions in long format
1525
 
_LONG_SEP = '-' * 60
1526
 
 
1527
 
 
1528
1379
class LongLogFormatter(LogFormatter):
1529
1380
 
1530
1381
    supports_merge_revisions = True
1531
 
    preferred_levels = 1
1532
1382
    supports_delta = True
1533
1383
    supports_tags = True
1534
1384
    supports_diff = True
1535
1385
 
1536
 
    def __init__(self, *args, **kwargs):
1537
 
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1538
 
        if self.show_timezone == 'original':
1539
 
            self.date_string = self._date_string_original_timezone
1540
 
        else:
1541
 
            self.date_string = self._date_string_with_timezone
1542
 
 
1543
 
    def _date_string_with_timezone(self, rev):
1544
 
        return format_date(rev.timestamp, rev.timezone or 0,
1545
 
                           self.show_timezone)
1546
 
 
1547
 
    def _date_string_original_timezone(self, rev):
1548
 
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1549
 
            rev.timezone or 0)
1550
 
 
1551
1386
    def log_revision(self, revision):
1552
1387
        """Log a revision, either merged or not."""
1553
1388
        indent = '    ' * revision.merge_depth
1554
 
        lines = [_LONG_SEP]
 
1389
        to_file = self.to_file
 
1390
        to_file.write(indent + '-' * 60 + '\n')
1555
1391
        if revision.revno is not None:
1556
 
            lines.append('revno: %s%s' % (revision.revno,
1557
 
                self.merge_marker(revision)))
 
1392
            to_file.write(indent + 'revno: %s\n' % (revision.revno,))
1558
1393
        if revision.tags:
1559
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1394
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1560
1395
        if self.show_ids:
1561
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1396
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
 
1397
            to_file.write('\n')
1562
1398
            for parent_id in revision.rev.parent_ids:
1563
 
                lines.append('parent: %s' % (parent_id,))
1564
 
        lines.extend(self.custom_properties(revision.rev))
 
1399
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
 
1400
        self.show_properties(revision.rev, indent)
1565
1401
 
1566
1402
        committer = revision.rev.committer
1567
 
        authors = self.authors(revision.rev, 'all')
 
1403
        authors = revision.rev.get_apparent_authors()
1568
1404
        if authors != [committer]:
1569
 
            lines.append('author: %s' % (", ".join(authors),))
1570
 
        lines.append('committer: %s' % (committer,))
 
1405
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
 
1406
        to_file.write(indent + 'committer: %s\n' % (committer,))
1571
1407
 
1572
1408
        branch_nick = revision.rev.properties.get('branch-nick', None)
1573
1409
        if branch_nick is not None:
1574
 
            lines.append('branch nick: %s' % (branch_nick,))
1575
 
 
1576
 
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1577
 
 
1578
 
        lines.append('message:')
 
1410
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
 
1411
 
 
1412
        date_str = format_date(revision.rev.timestamp,
 
1413
                               revision.rev.timezone or 0,
 
1414
                               self.show_timezone)
 
1415
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
 
1416
 
 
1417
        to_file.write(indent + 'message:\n')
1579
1418
        if not revision.rev.message:
1580
 
            lines.append('  (no message)')
 
1419
            to_file.write(indent + '  (no message)\n')
1581
1420
        else:
1582
1421
            message = revision.rev.message.rstrip('\r\n')
1583
1422
            for l in message.split('\n'):
1584
 
                lines.append('  %s' % (l,))
1585
 
 
1586
 
        # Dump the output, appending the delta and diff if requested
1587
 
        to_file = self.to_file
1588
 
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
 
1423
                to_file.write(indent + '  %s\n' % (l,))
1589
1424
        if revision.delta is not None:
1590
 
            # Use the standard status output to display changes
1591
 
            from bzrlib.delta import report_delta
1592
 
            report_delta(to_file, revision.delta, short_status=False, 
1593
 
                         show_ids=self.show_ids, indent=indent)
 
1425
            # We don't respect delta_format for compatibility
 
1426
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1427
                                short_status=False)
1594
1428
        if revision.diff is not None:
1595
1429
            to_file.write(indent + 'diff:\n')
1596
 
            to_file.flush()
1597
1430
            # Note: we explicitly don't indent the diff (relative to the
1598
1431
            # revision information) so that the output can be fed to patch -p0
1599
1432
            self.show_diff(self.to_exact_file, revision.diff, indent)
1600
 
            self.to_exact_file.flush()
1601
 
 
1602
 
    def get_advice_separator(self):
1603
 
        """Get the text separating the log from the closing advice."""
1604
 
        return '-' * 60 + '\n'
1605
1433
 
1606
1434
 
1607
1435
class ShortLogFormatter(LogFormatter):
1637
1465
        offset = ' ' * (revno_width + 1)
1638
1466
 
1639
1467
        to_file = self.to_file
 
1468
        is_merge = ''
 
1469
        if len(revision.rev.parent_ids) > 1:
 
1470
            is_merge = ' [merge]'
1640
1471
        tags = ''
1641
1472
        if revision.tags:
1642
1473
            tags = ' {%s}' % (', '.join(revision.tags))
1646
1477
                            revision.rev.timezone or 0,
1647
1478
                            self.show_timezone, date_fmt="%Y-%m-%d",
1648
1479
                            show_offset=False),
1649
 
                tags, self.merge_marker(revision)))
 
1480
                tags, is_merge))
1650
1481
        self.show_properties(revision.rev, indent+offset)
1651
1482
        if self.show_ids:
1652
1483
            to_file.write(indent + offset + 'revision-id:%s\n'
1659
1490
                to_file.write(indent + offset + '%s\n' % (l,))
1660
1491
 
1661
1492
        if revision.delta is not None:
1662
 
            # Use the standard status output to display changes
1663
 
            from bzrlib.delta import report_delta
1664
 
            report_delta(to_file, revision.delta, 
1665
 
                         short_status=self.delta_format==1, 
1666
 
                         show_ids=self.show_ids, indent=indent + offset)
 
1493
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1494
                                short_status=self.delta_format==1)
1667
1495
        if revision.diff is not None:
1668
1496
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1669
1497
        to_file.write('\n')
1677
1505
 
1678
1506
    def __init__(self, *args, **kwargs):
1679
1507
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1680
 
        width = terminal_width()
1681
 
        if width is not None:
1682
 
            # we need one extra space for terminals that wrap on last char
1683
 
            width = width - 1
1684
 
        self._max_chars = width
 
1508
        self._max_chars = terminal_width() - 1
1685
1509
 
1686
1510
    def truncate(self, str, max_len):
1687
 
        if max_len is None or len(str) <= max_len:
 
1511
        if len(str) <= max_len:
1688
1512
            return str
1689
 
        return str[:max_len-3] + '...'
 
1513
        return str[:max_len-3]+'...'
1690
1514
 
1691
1515
    def date_string(self, rev):
1692
1516
        return format_date(rev.timestamp, rev.timezone or 0,
1744
1568
                               self.show_timezone,
1745
1569
                               date_fmt='%Y-%m-%d',
1746
1570
                               show_offset=False)
1747
 
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1748
 
        committer_str = committer_str.replace(' <', '  <')
 
1571
        committer_str = revision.rev.committer.replace (' <', '  <')
1749
1572
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1750
1573
 
1751
1574
        if revision.delta is not None and revision.delta.has_changed():
1816
1639
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1817
1640
 
1818
1641
 
1819
 
def author_list_all(rev):
1820
 
    return rev.get_apparent_authors()[:]
1821
 
 
1822
 
 
1823
 
def author_list_first(rev):
1824
 
    lst = rev.get_apparent_authors()
1825
 
    try:
1826
 
        return [lst[0]]
1827
 
    except IndexError:
1828
 
        return []
1829
 
 
1830
 
 
1831
 
def author_list_committer(rev):
1832
 
    return [rev.committer]
1833
 
 
1834
 
 
1835
 
author_list_registry = registry.Registry()
1836
 
 
1837
 
author_list_registry.register('all', author_list_all,
1838
 
                              'All authors')
1839
 
 
1840
 
author_list_registry.register('first', author_list_first,
1841
 
                              'The first author')
1842
 
 
1843
 
author_list_registry.register('committer', author_list_committer,
1844
 
                              'The committer')
1845
 
 
1846
 
 
1847
1642
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1848
1643
    # deprecated; for compatibility
1849
1644
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
2000
1795
        lf.log_revision(lr)
2001
1796
 
2002
1797
 
2003
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
1798
def _get_info_for_log_files(revisionspec_list, file_list):
2004
1799
    """Find file-ids and kinds given a list of files and a revision range.
2005
1800
 
2006
1801
    We search for files at the end of the range. If not found there,
2010
1805
    :param file_list: the list of paths given on the command line;
2011
1806
      the first of these can be a branch location or a file path,
2012
1807
      the remainder must be file paths
2013
 
    :param add_cleanup: When the branch returned is read locked,
2014
 
      an unlock call will be queued to the cleanup.
2015
1808
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2016
1809
      info_list is a list of (relative_path, file_id, kind) tuples where
2017
1810
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2018
 
      branch will be read-locked.
2019
1811
    """
2020
 
    from builtins import _get_revision_range
 
1812
    from builtins import _get_revision_range, safe_relpath_files
2021
1813
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2022
 
    add_cleanup(b.lock_read().unlock)
2023
1814
    # XXX: It's damn messy converting a list of paths to relative paths when
2024
1815
    # those paths might be deleted ones, they might be on a case-insensitive
2025
1816
    # filesystem and/or they might be in silly locations (like another branch).
2029
1820
    # case of running log in a nested directory, assuming paths beyond the
2030
1821
    # first one haven't been deleted ...
2031
1822
    if tree:
2032
 
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
1823
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2033
1824
    else:
2034
1825
        relpaths = [path] + file_list[1:]
2035
1826
    info_list = []
2036
1827
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2037
1828
        "log")
2038
 
    if relpaths in ([], [u'']):
2039
 
        return b, [], start_rev_info, end_rev_info
2040
1829
    if start_rev_info is None and end_rev_info is None:
2041
1830
        if tree is None:
2042
1831
            tree = b.basis_tree()
2103
1892
 
2104
1893
 
2105
1894
properties_handler_registry = registry.Registry()
2106
 
 
2107
 
# Use the properties handlers to print out bug information if available
2108
 
def _bugs_properties_handler(revision):
2109
 
    if revision.properties.has_key('bugs'):
2110
 
        bug_lines = revision.properties['bugs'].split('\n')
2111
 
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2112
 
        fixed_bug_urls = [row[0] for row in bug_rows if
2113
 
                          len(row) > 1 and row[1] == 'fixed']
2114
 
 
2115
 
        if fixed_bug_urls:
2116
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2117
 
    return {}
2118
 
 
2119
 
properties_handler_registry.register('bugs_properties_handler',
2120
 
                                     _bugs_properties_handler)
 
1895
properties_handler_registry.register_lazy("foreign",
 
1896
                                          "bzrlib.foreign",
 
1897
                                          "show_foreign_properties")
2121
1898
 
2122
1899
 
2123
1900
# adapters which revision ids to log are filtered. When log is called, the