~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: John Arbash Meinel
  • Date: 2010-08-13 19:08:57 UTC
  • mto: (5050.17.7 2.2)
  • mto: This revision was merged to the branch mainline in revision 5379.
  • Revision ID: john@arbash-meinel.com-20100813190857-mvzwnimrxvm0zimp
Lots of documentation updates.

We had a lot of http links pointing to the old domain. They should
all now be properly updated to the new domain. (only bazaar-vcs.org
entry left is for pqm, which seems to still reside at the old url.)

Also removed one 'TODO' doc entry about switching to binary xdelta, since
we basically did just that with groupcompress.

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-2010 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
70
70
    diff,
71
71
    errors,
72
72
    foreign,
 
73
    osutils,
73
74
    repository as _mod_repository,
74
75
    revision as _mod_revision,
75
76
    revisionspec,
83
84
    )
84
85
from bzrlib.osutils import (
85
86
    format_date,
 
87
    format_date_with_offset_in_original_timezone,
86
88
    get_terminal_encoding,
87
 
    re_compile_checked,
88
89
    terminal_width,
89
90
    )
 
91
from bzrlib.symbol_versioning import (
 
92
    deprecated_function,
 
93
    deprecated_in,
 
94
    )
90
95
 
91
96
 
92
97
def find_touching_revisions(branch, file_id):
104
109
    last_path = None
105
110
    revno = 1
106
111
    for revision_id in branch.revision_history():
107
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
 
112
        this_inv = branch.repository.get_inventory(revision_id)
108
113
        if file_id in this_inv:
109
114
            this_ie = this_inv[file_id]
110
115
            this_path = this_inv.id2path(file_id)
215
220
    'direction': 'reverse',
216
221
    'levels': 1,
217
222
    'generate_tags': True,
 
223
    'exclude_common_ancestry': False,
218
224
    '_match_using_deltas': True,
219
225
    }
220
226
 
221
227
 
222
228
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):
 
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
                          ):
226
235
    """Convenience function for making a logging request dictionary.
227
236
 
228
237
    Using this function may make code slightly safer by ensuring
266
275
      algorithm used for matching specific_fileids. This parameter
267
276
      may be removed in the future so bzrlib client code should NOT
268
277
      use it.
 
278
 
 
279
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
 
280
      range operator or as a graph difference.
269
281
    """
270
282
    return {
271
283
        'direction': direction,
278
290
        'generate_tags': generate_tags,
279
291
        'delta_type': delta_type,
280
292
        'diff_type': diff_type,
 
293
        'exclude_common_ancestry': exclude_common_ancestry,
281
294
        # Add 'private' attributes for features that may be deprecated
282
295
        '_match_using_deltas': _match_using_deltas,
283
296
    }
303
316
 
304
317
 
305
318
class Logger(object):
306
 
    """An object the generates, formats and displays a log."""
 
319
    """An object that generates, formats and displays a log."""
307
320
 
308
321
    def __init__(self, branch, rqst):
309
322
        """Create a Logger.
384
397
        :return: An iterator yielding LogRevision objects.
385
398
        """
386
399
        rqst = self.rqst
 
400
        levels = rqst.get('levels')
 
401
        limit = rqst.get('limit')
 
402
        diff_type = rqst.get('diff_type')
387
403
        log_count = 0
388
404
        revision_iterator = self._create_log_revision_iterator()
389
405
        for revs in revision_iterator:
390
406
            for (rev_id, revno, merge_depth), rev, delta in revs:
391
407
                # 0 levels means show everything; merge_depth counts from 0
392
 
                levels = rqst.get('levels')
393
408
                if levels != 0 and merge_depth >= levels:
394
409
                    continue
395
 
                diff = self._format_diff(rev, rev_id)
 
410
                if diff_type is None:
 
411
                    diff = None
 
412
                else:
 
413
                    diff = self._format_diff(rev, rev_id, diff_type)
396
414
                yield LogRevision(rev, revno, merge_depth, delta,
397
415
                    self.rev_tag_dict.get(rev_id), diff)
398
 
                limit = rqst.get('limit')
399
416
                if limit:
400
417
                    log_count += 1
401
418
                    if log_count >= limit:
402
419
                        return
403
420
 
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
 
421
    def _format_diff(self, rev, rev_id, diff_type):
408
422
        repo = self.branch.repository
409
423
        if len(rev.parent_ids) == 0:
410
424
            ancestor_id = _mod_revision.NULL_REVISION
418
432
        else:
419
433
            specific_files = None
420
434
        s = StringIO()
 
435
        path_encoding = osutils.get_diff_header_encoding()
421
436
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
422
 
            new_label='')
 
437
            new_label='', path_encoding=path_encoding)
423
438
        return s.getvalue()
424
439
 
425
440
    def _create_log_revision_iterator(self):
449
464
        generate_merge_revisions = rqst.get('levels') != 1
450
465
        delayed_graph_generation = not rqst.get('specific_fileids') and (
451
466
                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)
 
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'))
455
473
 
456
474
        # Apply the other filters
457
475
        return make_log_rev_iterator(self.branch, view_revisions,
464
482
        # Note that we always generate the merge revisions because
465
483
        # filter_revisions_touching_file_id() requires them ...
466
484
        rqst = self.rqst
467
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
468
 
            self.end_rev_id, rqst.get('direction'), True)
 
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'))
469
489
        if not isinstance(view_revisions, list):
470
490
            view_revisions = list(view_revisions)
471
491
        view_revisions = _filter_revisions_touching_file_id(self.branch,
476
496
 
477
497
 
478
498
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
479
 
    generate_merge_revisions, delayed_graph_generation=False):
 
499
                         generate_merge_revisions,
 
500
                         delayed_graph_generation=False,
 
501
                         exclude_common_ancestry=False,
 
502
                         ):
480
503
    """Calculate the revisions to view.
481
504
 
482
505
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
483
506
             a list of the same tuples.
484
507
    """
 
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)
485
513
    br_revno, br_rev_id = branch.last_revision_info()
486
514
    if br_revno == 0:
487
515
        return []
488
516
 
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)
 
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)
499
529
    else:
500
 
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
501
 
            direction, delayed_graph_generation)
 
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
502
536
 
503
537
 
504
538
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
511
545
        return [(rev_id, revno_str, 0)]
512
546
 
513
547
 
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)
 
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)
516
553
    # If a start limit was given and it's not obviously an
517
554
    # ancestor of the end limit, check it before outputting anything
518
555
    if direction == 'forward' or (start_rev_id
522
559
        except _StartNotLinearAncestor:
523
560
            raise errors.BzrCommandError('Start revision not found in'
524
561
                ' left-hand history of end revision.')
525
 
    if direction == 'forward':
526
 
        result = reversed(result)
527
562
    return result
528
563
 
529
564
 
530
565
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
531
 
    delayed_graph_generation):
 
566
                            delayed_graph_generation,
 
567
                            exclude_common_ancestry=False):
532
568
    # On large trees, generating the merge graph can take 30-60 seconds
533
569
    # so we delay doing it until a merge is detected, incrementally
534
570
    # 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
535
575
    initial_revisions = []
536
576
    if delayed_graph_generation:
537
577
        try:
538
 
            for rev_id, revno, depth in \
539
 
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
578
            for rev_id, revno, depth in  _linear_view_revisions(
 
579
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
540
580
                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.
541
596
                    end_rev_id = rev_id
542
597
                    break
543
598
                else:
544
599
                    initial_revisions.append((rev_id, revno, depth))
545
600
            else:
546
601
                # 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)
 
602
                return initial_revisions
553
603
        except _StartNotLinearAncestor:
554
604
            # A merge was never detected so the lower revision limit can't
555
605
            # be nested down somewhere
556
606
            raise errors.BzrCommandError('Start revision not found in'
557
607
                ' history of end revision.')
558
608
 
 
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
 
559
612
    # A log including nested merges is required. If the direction is reverse,
560
613
    # we rebase the initial merge depths so that the development line is
561
614
    # shown naturally, i.e. just like it is for linear logging. We can easily
563
616
    # indented at the end seems slightly nicer in that case.
564
617
    view_revisions = chain(iter(initial_revisions),
565
618
        _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)
 
619
                              rebase_initial_depths=(direction == 'reverse'),
 
620
                              exclude_common_ancestry=exclude_common_ancestry))
 
621
    return view_revisions
575
622
 
576
623
 
577
624
def _has_merges(branch, rev_id):
595
642
        else:
596
643
            # not obvious
597
644
            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.
598
647
    return True
599
648
 
600
649
 
601
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
650
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
651
                           exclude_common_ancestry=False):
602
652
    """Calculate a sequence of revisions to view, newest to oldest.
603
653
 
604
654
    :param start_rev_id: the lower revision-id
605
655
    :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.
606
658
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
607
659
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
608
 
      is not found walking the left-hand history
 
660
        is not found walking the left-hand history
609
661
    """
610
662
    br_revno, br_rev_id = branch.last_revision_info()
611
663
    repo = branch.repository
622
674
            revno = branch.revision_id_to_dotted_revno(revision_id)
623
675
            revno_str = '.'.join(str(n) for n in revno)
624
676
            if not found_start and revision_id == start_rev_id:
625
 
                yield revision_id, revno_str, 0
 
677
                if not exclude_common_ancestry:
 
678
                    yield revision_id, revno_str, 0
626
679
                found_start = True
627
680
                break
628
681
            else:
633
686
 
634
687
 
635
688
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
636
 
    rebase_initial_depths=True):
 
689
                          rebase_initial_depths=True,
 
690
                          exclude_common_ancestry=False):
637
691
    """Calculate revisions to view including merges, newest to oldest.
638
692
 
639
693
    :param branch: the branch
643
697
      revision is found?
644
698
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
645
699
    """
 
700
    if exclude_common_ancestry:
 
701
        stop_rule = 'with-merges-without-common-ancestry'
 
702
    else:
 
703
        stop_rule = 'with-merges'
646
704
    view_revisions = branch.iter_merge_sorted_revisions(
647
705
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
648
 
        stop_rule="with-merges")
 
706
        stop_rule=stop_rule)
649
707
    if not rebase_initial_depths:
650
708
        for (rev_id, merge_depth, revno, end_of_merge
651
709
             ) in view_revisions:
662
720
                depth_adjustment = merge_depth
663
721
            if depth_adjustment:
664
722
                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.
665
727
                    depth_adjustment = merge_depth
666
728
                merge_depth -= depth_adjustment
667
729
            yield rev_id, '.'.join(map(str, revno)), merge_depth
668
730
 
669
731
 
 
732
@deprecated_function(deprecated_in((2, 2, 0)))
670
733
def calculate_view_revisions(branch, start_revision, end_revision, direction,
671
734
        specific_fileid, generate_merge_revisions):
672
735
    """Calculate the revisions to view.
674
737
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
675
738
             a list of the same tuples.
676
739
    """
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
740
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
681
741
        end_revision)
682
742
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
750
810
    """
751
811
    if search is None:
752
812
        return log_rev_iterator
753
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
754
 
            'log message filter')
 
813
    searchRE = re.compile(search, re.IGNORECASE)
755
814
    return _filter_message_re(searchRE, log_rev_iterator)
756
815
 
757
816
 
1032
1091
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1033
1092
 
1034
1093
 
 
1094
@deprecated_function(deprecated_in((2, 2, 0)))
1035
1095
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1036
1096
    """Filter view_revisions based on revision ranges.
1037
1097
 
1046
1106
 
1047
1107
    :return: The filtered view_revisions.
1048
1108
    """
1049
 
    # This method is no longer called by the main code path.
1050
 
    # It may be removed soon. IGC 20090127
1051
1109
    if start_rev_id or end_rev_id:
1052
1110
        revision_ids = [r for r, n, d in view_revisions]
1053
1111
        if start_rev_id:
1159
1217
    return result
1160
1218
 
1161
1219
 
 
1220
@deprecated_function(deprecated_in((2, 2, 0)))
1162
1221
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1163
1222
                       include_merges=True):
1164
1223
    """Produce an iterator of revisions to show
1165
1224
    :return: an iterator of (revision_id, revno, merge_depth)
1166
1225
    (if there is no revno for a revision, None is supplied)
1167
1226
    """
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
1227
    if not include_merges:
1172
1228
        revision_ids = mainline_revs[1:]
1173
1229
        if direction == 'reverse':
1291
1347
    preferred_levels = 0
1292
1348
 
1293
1349
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1294
 
                 delta_format=None, levels=None, show_advice=False):
 
1350
                 delta_format=None, levels=None, show_advice=False,
 
1351
                 to_exact_file=None, author_list_handler=None):
1295
1352
        """Create a LogFormatter.
1296
1353
 
1297
1354
        :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.
1298
1357
        :param show_ids: if True, revision-ids are to be displayed
1299
1358
        :param show_timezone: the timezone to use
1300
1359
        :param delta_format: the level of delta information to display
1303
1362
          let the log formatter decide.
1304
1363
        :param show_advice: whether to show advice at the end of the
1305
1364
          log or not
 
1365
        :param author_list_handler: callable generating a list of
 
1366
          authors to display for a given revision
1306
1367
        """
1307
1368
        self.to_file = to_file
1308
1369
        # 'exact' stream used to show diff, it should print content 'as is'
1309
1370
        # 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)
 
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)
1311
1378
        self.show_ids = show_ids
1312
1379
        self.show_timezone = show_timezone
1313
1380
        if delta_format is None:
1317
1384
        self.levels = levels
1318
1385
        self._show_advice = show_advice
1319
1386
        self._merge_count = 0
 
1387
        self._author_list_handler = author_list_handler
1320
1388
 
1321
1389
    def get_levels(self):
1322
1390
        """Get the number of levels to display or 0 for all."""
1354
1422
        return address
1355
1423
 
1356
1424
    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
 
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
1361
1460
 
1362
1461
    def merge_marker(self, revision):
1363
1462
        """Get the merge marker to include in the output or '' if none."""
1367
1466
        else:
1368
1467
            return ''
1369
1468
 
1370
 
    def show_foreign_info(self, rev, indent):
 
1469
    def show_properties(self, revision, indent):
 
1470
        """Displays the custom properties returned by each registered handler.
 
1471
 
 
1472
        If a registered handler raises an error it is propagated.
 
1473
        """
 
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
        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):
1371
1490
        """Custom log displayer for foreign revision identifiers.
1372
1491
 
1373
1492
        :param rev: Revision object.
1374
1493
        """
1375
1494
        # Revision comes directly from a foreign repository
1376
1495
        if isinstance(rev, foreign.ForeignRevision):
1377
 
            self._write_properties(indent, rev.mapping.vcs.show_foreign_revid(
1378
 
                rev.foreign_revid))
1379
 
            return
 
1496
            return self._format_properties(
 
1497
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1380
1498
 
1381
1499
        # Imported foreign revision revision ids always contain :
1382
1500
        if not ":" in rev.revision_id:
1383
 
            return
 
1501
            return []
1384
1502
 
1385
1503
        # Revision was once imported from a foreign repository
1386
1504
        try:
1387
1505
            foreign_revid, mapping = \
1388
1506
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1389
1507
        except errors.InvalidRevisionId:
1390
 
            return
 
1508
            return []
1391
1509
 
1392
 
        self._write_properties(indent, 
 
1510
        return self._format_properties(
1393
1511
            mapping.vcs.show_foreign_revid(foreign_revid))
1394
1512
 
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):
 
1513
    def _format_properties(self, properties):
 
1514
        lines = []
1404
1515
        for key, value in properties.items():
1405
 
            self.to_file.write(indent + key + ': ' + value + '\n')
 
1516
            lines.append(key + ': ' + value)
 
1517
        return lines
1406
1518
 
1407
1519
    def show_diff(self, to_file, diff, indent):
1408
1520
        for l in diff.rstrip().split('\n'):
1409
1521
            to_file.write(indent + '%s\n' % (l,))
1410
1522
 
1411
1523
 
 
1524
# Separator between revisions in long format
 
1525
_LONG_SEP = '-' * 60
 
1526
 
 
1527
 
1412
1528
class LongLogFormatter(LogFormatter):
1413
1529
 
1414
1530
    supports_merge_revisions = True
1417
1533
    supports_tags = True
1418
1534
    supports_diff = True
1419
1535
 
 
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
 
1420
1551
    def log_revision(self, revision):
1421
1552
        """Log a revision, either merged or not."""
1422
1553
        indent = '    ' * revision.merge_depth
1423
 
        to_file = self.to_file
1424
 
        to_file.write(indent + '-' * 60 + '\n')
 
1554
        lines = [_LONG_SEP]
1425
1555
        if revision.revno is not None:
1426
 
            to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
 
1556
            lines.append('revno: %s%s' % (revision.revno,
1427
1557
                self.merge_marker(revision)))
1428
1558
        if revision.tags:
1429
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
 
1559
            lines.append('tags: %s' % (', '.join(revision.tags)))
1430
1560
        if self.show_ids:
1431
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1432
 
            to_file.write('\n')
 
1561
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1433
1562
            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)
 
1563
                lines.append('parent: %s' % (parent_id,))
 
1564
        lines.extend(self.custom_properties(revision.rev))
1437
1565
 
1438
1566
        committer = revision.rev.committer
1439
 
        authors = revision.rev.get_apparent_authors()
 
1567
        authors = self.authors(revision.rev, 'all')
1440
1568
        if authors != [committer]:
1441
 
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1442
 
        to_file.write(indent + 'committer: %s\n' % (committer,))
 
1569
            lines.append('author: %s' % (", ".join(authors),))
 
1570
        lines.append('committer: %s' % (committer,))
1443
1571
 
1444
1572
        branch_nick = revision.rev.properties.get('branch-nick', None)
1445
1573
        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')
 
1574
            lines.append('branch nick: %s' % (branch_nick,))
 
1575
 
 
1576
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1577
 
 
1578
        lines.append('message:')
1454
1579
        if not revision.rev.message:
1455
 
            to_file.write(indent + '  (no message)\n')
 
1580
            lines.append('  (no message)')
1456
1581
        else:
1457
1582
            message = revision.rev.message.rstrip('\r\n')
1458
1583
            for l in message.split('\n'):
1459
 
                to_file.write(indent + '  %s\n' % (l,))
 
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)))
1460
1589
        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)
 
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)
1464
1594
        if revision.diff is not None:
1465
1595
            to_file.write(indent + 'diff:\n')
 
1596
            to_file.flush()
1466
1597
            # Note: we explicitly don't indent the diff (relative to the
1467
1598
            # revision information) so that the output can be fed to patch -p0
1468
1599
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1600
            self.to_exact_file.flush()
1469
1601
 
1470
1602
    def get_advice_separator(self):
1471
1603
        """Get the text separating the log from the closing advice."""
1515
1647
                            self.show_timezone, date_fmt="%Y-%m-%d",
1516
1648
                            show_offset=False),
1517
1649
                tags, self.merge_marker(revision)))
1518
 
        self.show_foreign_info(revision.rev, indent+offset)
1519
1650
        self.show_properties(revision.rev, indent+offset)
1520
1651
        if self.show_ids:
1521
1652
            to_file.write(indent + offset + 'revision-id:%s\n'
1528
1659
                to_file.write(indent + offset + '%s\n' % (l,))
1529
1660
 
1530
1661
        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)
 
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)
1533
1667
        if revision.diff is not None:
1534
1668
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1535
1669
        to_file.write('\n')
1543
1677
 
1544
1678
    def __init__(self, *args, **kwargs):
1545
1679
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1546
 
        self._max_chars = terminal_width() - 1
 
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
1547
1685
 
1548
1686
    def truncate(self, str, max_len):
1549
 
        if len(str) <= max_len:
 
1687
        if max_len is None or len(str) <= max_len:
1550
1688
            return str
1551
 
        return str[:max_len-3]+'...'
 
1689
        return str[:max_len-3] + '...'
1552
1690
 
1553
1691
    def date_string(self, rev):
1554
1692
        return format_date(rev.timestamp, rev.timezone or 0,
1606
1744
                               self.show_timezone,
1607
1745
                               date_fmt='%Y-%m-%d',
1608
1746
                               show_offset=False)
1609
 
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1747
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1748
        committer_str = committer_str.replace(' <', '  <')
1610
1749
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1611
1750
 
1612
1751
        if revision.delta is not None and revision.delta.has_changed():
1677
1816
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1678
1817
 
1679
1818
 
 
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
 
1680
1847
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1681
1848
    # deprecated; for compatibility
1682
1849
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1833
2000
        lf.log_revision(lr)
1834
2001
 
1835
2002
 
1836
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2003
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1837
2004
    """Find file-ids and kinds given a list of files and a revision range.
1838
2005
 
1839
2006
    We search for files at the end of the range. If not found there,
1843
2010
    :param file_list: the list of paths given on the command line;
1844
2011
      the first of these can be a branch location or a file path,
1845
2012
      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.
1846
2015
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1847
2016
      info_list is a list of (relative_path, file_id, kind) tuples where
1848
2017
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
2018
      branch will be read-locked.
1849
2019
    """
1850
2020
    from builtins import _get_revision_range, safe_relpath_files
1851
2021
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
2022
    add_cleanup(b.lock_read().unlock)
1852
2023
    # XXX: It's damn messy converting a list of paths to relative paths when
1853
2024
    # those paths might be deleted ones, they might be on a case-insensitive
1854
2025
    # filesystem and/or they might be in silly locations (like another branch).
1933
2104
 
1934
2105
properties_handler_registry = registry.Registry()
1935
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)
 
2121
 
1936
2122
 
1937
2123
# adapters which revision ids to log are filtered. When log is called, the
1938
2124
# log_rev_iterator is adapted through each of these factory methods.