~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: John Arbash Meinel
  • Date: 2009-06-12 18:05:15 UTC
  • mto: (4371.4.5 vila-better-heads)
  • mto: This revision was merged to the branch mainline in revision 4449.
  • Revision ID: john@arbash-meinel.com-20090612180515-t0cwbjsnve094oik
Add a failing test for handling nodes that are in the same linear chain.

It fails because the ancestry skipping causes us to miss the fact that the two nodes
are actually directly related. We could check at the beginning, as the 
code used to do, but I think that will be incomplete for the more-than-two
heads cases.

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
83
83
    )
84
84
from bzrlib.osutils import (
85
85
    format_date,
86
 
    format_date_with_offset_in_original_timezone,
87
86
    get_terminal_encoding,
88
87
    re_compile_checked,
89
88
    terminal_width,
90
89
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
94
 
    )
95
90
 
96
91
 
97
92
def find_touching_revisions(branch, file_id):
109
104
    last_path = None
110
105
    revno = 1
111
106
    for revision_id in branch.revision_history():
112
 
        this_inv = branch.repository.get_inventory(revision_id)
 
107
        this_inv = branch.repository.get_revision_inventory(revision_id)
113
108
        if file_id in this_inv:
114
109
            this_ie = this_inv[file_id]
115
110
            this_path = this_inv.id2path(file_id)
220
215
    'direction': 'reverse',
221
216
    'levels': 1,
222
217
    'generate_tags': True,
223
 
    'exclude_common_ancestry': False,
224
218
    '_match_using_deltas': True,
225
219
    }
226
220
 
227
221
 
228
222
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
 
                          ):
 
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):
235
226
    """Convenience function for making a logging request dictionary.
236
227
 
237
228
    Using this function may make code slightly safer by ensuring
275
266
      algorithm used for matching specific_fileids. This parameter
276
267
      may be removed in the future so bzrlib client code should NOT
277
268
      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
269
    """
282
270
    return {
283
271
        'direction': direction,
290
278
        'generate_tags': generate_tags,
291
279
        'delta_type': delta_type,
292
280
        'diff_type': diff_type,
293
 
        'exclude_common_ancestry': exclude_common_ancestry,
294
281
        # Add 'private' attributes for features that may be deprecated
295
282
        '_match_using_deltas': _match_using_deltas,
296
283
    }
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.
397
384
        :return: An iterator yielding LogRevision objects.
398
385
        """
399
386
        rqst = self.rqst
400
 
        levels = rqst.get('levels')
401
 
        limit = rqst.get('limit')
402
 
        diff_type = rqst.get('diff_type')
403
387
        log_count = 0
404
388
        revision_iterator = self._create_log_revision_iterator()
405
389
        for revs in revision_iterator:
406
390
            for (rev_id, revno, merge_depth), rev, delta in revs:
407
391
                # 0 levels means show everything; merge_depth counts from 0
 
392
                levels = rqst.get('levels')
408
393
                if levels != 0 and merge_depth >= levels:
409
394
                    continue
410
 
                if diff_type is None:
411
 
                    diff = None
412
 
                else:
413
 
                    diff = self._format_diff(rev, rev_id, diff_type)
 
395
                diff = self._format_diff(rev, rev_id)
414
396
                yield LogRevision(rev, revno, merge_depth, delta,
415
397
                    self.rev_tag_dict.get(rev_id), diff)
 
398
                limit = rqst.get('limit')
416
399
                if limit:
417
400
                    log_count += 1
418
401
                    if log_count >= limit:
419
402
                        return
420
403
 
421
 
    def _format_diff(self, rev, rev_id, diff_type):
 
404
    def _format_diff(self, rev, rev_id):
 
405
        diff_type = self.rqst.get('diff_type')
 
406
        if diff_type is None:
 
407
            return None
422
408
        repo = self.branch.repository
423
409
        if len(rev.parent_ids) == 0:
424
410
            ancestor_id = _mod_revision.NULL_REVISION
463
449
        generate_merge_revisions = rqst.get('levels') != 1
464
450
        delayed_graph_generation = not rqst.get('specific_fileids') and (
465
451
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
466
 
        view_revisions = _calc_view_revisions(
467
 
            self.branch, self.start_rev_id, self.end_rev_id,
468
 
            rqst.get('direction'),
469
 
            generate_merge_revisions=generate_merge_revisions,
470
 
            delayed_graph_generation=delayed_graph_generation,
471
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
452
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
453
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
 
454
            delayed_graph_generation=delayed_graph_generation)
472
455
 
473
456
        # Apply the other filters
474
457
        return make_log_rev_iterator(self.branch, view_revisions,
481
464
        # Note that we always generate the merge revisions because
482
465
        # filter_revisions_touching_file_id() requires them ...
483
466
        rqst = self.rqst
484
 
        view_revisions = _calc_view_revisions(
485
 
            self.branch, self.start_rev_id, self.end_rev_id,
486
 
            rqst.get('direction'), generate_merge_revisions=True,
487
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
467
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
468
            self.end_rev_id, rqst.get('direction'), True)
488
469
        if not isinstance(view_revisions, list):
489
470
            view_revisions = list(view_revisions)
490
471
        view_revisions = _filter_revisions_touching_file_id(self.branch,
495
476
 
496
477
 
497
478
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
498
 
                         generate_merge_revisions,
499
 
                         delayed_graph_generation=False,
500
 
                         exclude_common_ancestry=False,
501
 
                         ):
 
479
    generate_merge_revisions, delayed_graph_generation=False):
502
480
    """Calculate the revisions to view.
503
481
 
504
482
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
505
483
             a list of the same tuples.
506
484
    """
507
 
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
 
        raise errors.BzrCommandError(
509
 
            '--exclude-common-ancestry requires two different revisions')
510
 
    if direction not in ('reverse', 'forward'):
511
 
        raise ValueError('invalid direction %r' % direction)
512
485
    br_revno, br_rev_id = branch.last_revision_info()
513
486
    if br_revno == 0:
514
487
        return []
515
488
 
516
 
    if (end_rev_id and start_rev_id == end_rev_id
517
 
        and (not generate_merge_revisions
518
 
             or not _has_merges(branch, end_rev_id))):
519
 
        # If a single revision is requested, check we can handle it
520
 
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
521
 
                                           br_revno)
522
 
    elif not generate_merge_revisions:
523
 
        # If we only want to see linear revisions, we can iterate ...
524
 
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
525
 
                                             direction)
526
 
        if direction == 'forward':
527
 
            iter_revs = reversed(iter_revs)
 
489
    # If a single revision is requested, check we can handle it
 
490
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
 
491
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
 
492
    if generate_single_revision:
 
493
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
 
494
 
 
495
    # If we only want to see linear revisions, we can iterate ...
 
496
    if not generate_merge_revisions:
 
497
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
498
            direction)
528
499
    else:
529
 
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
530
 
                                            direction, delayed_graph_generation,
531
 
                                            exclude_common_ancestry)
532
 
        if direction == 'forward':
533
 
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
534
 
    return iter_revs
 
500
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
501
            direction, delayed_graph_generation)
535
502
 
536
503
 
537
504
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
555
522
        except _StartNotLinearAncestor:
556
523
            raise errors.BzrCommandError('Start revision not found in'
557
524
                ' left-hand history of end revision.')
 
525
    if direction == 'forward':
 
526
        result = reversed(result)
558
527
    return result
559
528
 
560
529
 
561
530
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
 
                            delayed_graph_generation,
563
 
                            exclude_common_ancestry=False):
 
531
    delayed_graph_generation):
564
532
    # On large trees, generating the merge graph can take 30-60 seconds
565
533
    # so we delay doing it until a merge is detected, incrementally
566
534
    # returning initial (non-merge) revisions while we can.
567
 
 
568
 
    # The above is only true for old formats (<= 0.92), for newer formats, a
569
 
    # couple of seconds only should be needed to load the whole graph and the
570
 
    # other graph operations needed are even faster than that -- vila 100201
571
535
    initial_revisions = []
572
536
    if delayed_graph_generation:
573
537
        try:
574
 
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
538
            for rev_id, revno, depth in \
 
539
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
576
540
                if _has_merges(branch, rev_id):
577
 
                    # The end_rev_id can be nested down somewhere. We need an
578
 
                    # explicit ancestry check. There is an ambiguity here as we
579
 
                    # may not raise _StartNotLinearAncestor for a revision that
580
 
                    # is an ancestor but not a *linear* one. But since we have
581
 
                    # loaded the graph to do the check (or calculate a dotted
582
 
                    # revno), we may as well accept to show the log...  We need
583
 
                    # the check only if start_rev_id is not None as all
584
 
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
585
 
                    # -- vila 20100319
586
 
                    graph = branch.repository.get_graph()
587
 
                    if (start_rev_id is not None
588
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
589
 
                        raise _StartNotLinearAncestor()
590
 
                    # Since we collected the revisions so far, we need to
591
 
                    # adjust end_rev_id.
592
541
                    end_rev_id = rev_id
593
542
                    break
594
543
                else:
595
544
                    initial_revisions.append((rev_id, revno, depth))
596
545
            else:
597
546
                # No merged revisions found
598
 
                return initial_revisions
 
547
                if direction == 'reverse':
 
548
                    return initial_revisions
 
549
                elif direction == 'forward':
 
550
                    return reversed(initial_revisions)
 
551
                else:
 
552
                    raise ValueError('invalid direction %r' % direction)
599
553
        except _StartNotLinearAncestor:
600
554
            # A merge was never detected so the lower revision limit can't
601
555
            # be nested down somewhere
602
556
            raise errors.BzrCommandError('Start revision not found in'
603
557
                ' history of end revision.')
604
558
 
605
 
    # We exit the loop above because we encounter a revision with merges, from
606
 
    # this revision, we need to switch to _graph_view_revisions.
607
 
 
608
559
    # A log including nested merges is required. If the direction is reverse,
609
560
    # we rebase the initial merge depths so that the development line is
610
561
    # shown naturally, i.e. just like it is for linear logging. We can easily
612
563
    # indented at the end seems slightly nicer in that case.
613
564
    view_revisions = chain(iter(initial_revisions),
614
565
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
615
 
                              rebase_initial_depths=(direction == 'reverse'),
616
 
                              exclude_common_ancestry=exclude_common_ancestry))
617
 
    return view_revisions
 
566
        rebase_initial_depths=direction == 'reverse'))
 
567
    if direction == 'reverse':
 
568
        return view_revisions
 
569
    elif direction == 'forward':
 
570
        # Forward means oldest first, adjusting for depth.
 
571
        view_revisions = reverse_by_depth(list(view_revisions))
 
572
        return _rebase_merge_depth(view_revisions)
 
573
    else:
 
574
        raise ValueError('invalid direction %r' % direction)
618
575
 
619
576
 
620
577
def _has_merges(branch, rev_id):
638
595
        else:
639
596
            # not obvious
640
597
            return False
641
 
    # if either start or end is not specified then we use either the first or
642
 
    # the last revision and *they* are obvious ancestors.
643
598
    return True
644
599
 
645
600
 
678
633
 
679
634
 
680
635
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
681
 
                          rebase_initial_depths=True,
682
 
                          exclude_common_ancestry=False):
 
636
    rebase_initial_depths=True):
683
637
    """Calculate revisions to view including merges, newest to oldest.
684
638
 
685
639
    :param branch: the branch
689
643
      revision is found?
690
644
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
691
645
    """
692
 
    if exclude_common_ancestry:
693
 
        stop_rule = 'with-merges-without-common-ancestry'
694
 
    else:
695
 
        stop_rule = 'with-merges'
696
646
    view_revisions = branch.iter_merge_sorted_revisions(
697
647
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
698
 
        stop_rule=stop_rule)
 
648
        stop_rule="with-merges")
699
649
    if not rebase_initial_depths:
700
650
        for (rev_id, merge_depth, revno, end_of_merge
701
651
             ) in view_revisions:
712
662
                depth_adjustment = merge_depth
713
663
            if depth_adjustment:
714
664
                if merge_depth < depth_adjustment:
715
 
                    # From now on we reduce the depth adjustement, this can be
716
 
                    # surprising for users. The alternative requires two passes
717
 
                    # which breaks the fast display of the first revision
718
 
                    # though.
719
665
                    depth_adjustment = merge_depth
720
666
                merge_depth -= depth_adjustment
721
667
            yield rev_id, '.'.join(map(str, revno)), merge_depth
722
668
 
723
669
 
724
 
@deprecated_function(deprecated_in((2, 2, 0)))
725
670
def calculate_view_revisions(branch, start_revision, end_revision, direction,
726
671
        specific_fileid, generate_merge_revisions):
727
672
    """Calculate the revisions to view.
729
674
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
730
675
             a list of the same tuples.
731
676
    """
 
677
    # This method is no longer called by the main code path.
 
678
    # It is retained for API compatibility and may be deprecated
 
679
    # soon. IGC 20090116
732
680
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
733
681
        end_revision)
734
682
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
1084
1032
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1085
1033
 
1086
1034
 
1087
 
@deprecated_function(deprecated_in((2, 2, 0)))
1088
1035
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1089
1036
    """Filter view_revisions based on revision ranges.
1090
1037
 
1099
1046
 
1100
1047
    :return: The filtered view_revisions.
1101
1048
    """
 
1049
    # This method is no longer called by the main code path.
 
1050
    # It may be removed soon. IGC 20090127
1102
1051
    if start_rev_id or end_rev_id:
1103
1052
        revision_ids = [r for r, n, d in view_revisions]
1104
1053
        if start_rev_id:
1210
1159
    return result
1211
1160
 
1212
1161
 
1213
 
@deprecated_function(deprecated_in((2, 2, 0)))
1214
1162
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1215
1163
                       include_merges=True):
1216
1164
    """Produce an iterator of revisions to show
1217
1165
    :return: an iterator of (revision_id, revno, merge_depth)
1218
1166
    (if there is no revno for a revision, None is supplied)
1219
1167
    """
 
1168
    # This method is no longer called by the main code path.
 
1169
    # It is retained for API compatibility and may be deprecated
 
1170
    # soon. IGC 20090127
1220
1171
    if not include_merges:
1221
1172
        revision_ids = mainline_revs[1:]
1222
1173
        if direction == 'reverse':
1340
1291
    preferred_levels = 0
1341
1292
 
1342
1293
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
 
                 delta_format=None, levels=None, show_advice=False,
1344
 
                 to_exact_file=None):
 
1294
                 delta_format=None, levels=None, show_advice=False):
1345
1295
        """Create a LogFormatter.
1346
1296
 
1347
1297
        :param to_file: the file to output to
1348
 
        :param to_exact_file: if set, gives an output stream to which 
1349
 
             non-Unicode diffs are written.
1350
1298
        :param show_ids: if True, revision-ids are to be displayed
1351
1299
        :param show_timezone: the timezone to use
1352
1300
        :param delta_format: the level of delta information to display
1359
1307
        self.to_file = to_file
1360
1308
        # 'exact' stream used to show diff, it should print content 'as is'
1361
1309
        # and should not try to decode/encode it to unicode to avoid bug #328007
1362
 
        if to_exact_file is not None:
1363
 
            self.to_exact_file = to_exact_file
1364
 
        else:
1365
 
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
1366
 
            # for code that expects to get diffs to pass in the exact file
1367
 
            # stream
1368
 
            self.to_exact_file = getattr(to_file, 'stream', to_file)
 
1310
        self.to_exact_file = getattr(to_file, 'stream', to_file)
1369
1311
        self.show_ids = show_ids
1370
1312
        self.show_timezone = show_timezone
1371
1313
        if delta_format is None:
1425
1367
        else:
1426
1368
            return ''
1427
1369
 
1428
 
    def show_properties(self, revision, indent):
1429
 
        """Displays the custom properties returned by each registered handler.
1430
 
 
1431
 
        If a registered handler raises an error it is propagated.
1432
 
        """
1433
 
        for line in self.custom_properties(revision):
1434
 
            self.to_file.write("%s%s\n" % (indent, line))
1435
 
 
1436
 
    def custom_properties(self, revision):
1437
 
        """Format the custom properties returned by each registered handler.
1438
 
 
1439
 
        If a registered handler raises an error it is propagated.
1440
 
 
1441
 
        :return: a list of formatted lines (excluding trailing newlines)
1442
 
        """
1443
 
        lines = self._foreign_info_properties(revision)
1444
 
        for key, handler in properties_handler_registry.iteritems():
1445
 
            lines.extend(self._format_properties(handler(revision)))
1446
 
        return lines
1447
 
 
1448
 
    def _foreign_info_properties(self, rev):
 
1370
    def show_foreign_info(self, rev, indent):
1449
1371
        """Custom log displayer for foreign revision identifiers.
1450
1372
 
1451
1373
        :param rev: Revision object.
1452
1374
        """
1453
1375
        # Revision comes directly from a foreign repository
1454
1376
        if isinstance(rev, foreign.ForeignRevision):
1455
 
            return self._format_properties(
1456
 
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1377
            self._write_properties(indent, rev.mapping.vcs.show_foreign_revid(
 
1378
                rev.foreign_revid))
 
1379
            return
1457
1380
 
1458
1381
        # Imported foreign revision revision ids always contain :
1459
1382
        if not ":" in rev.revision_id:
1460
 
            return []
 
1383
            return
1461
1384
 
1462
1385
        # Revision was once imported from a foreign repository
1463
1386
        try:
1464
1387
            foreign_revid, mapping = \
1465
1388
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1466
1389
        except errors.InvalidRevisionId:
1467
 
            return []
 
1390
            return
1468
1391
 
1469
 
        return self._format_properties(
 
1392
        self._write_properties(indent, 
1470
1393
            mapping.vcs.show_foreign_revid(foreign_revid))
1471
1394
 
1472
 
    def _format_properties(self, properties):
1473
 
        lines = []
 
1395
    def show_properties(self, revision, indent):
 
1396
        """Displays the custom properties returned by each registered handler.
 
1397
 
 
1398
        If a registered handler raises an error it is propagated.
 
1399
        """
 
1400
        for key, handler in properties_handler_registry.iteritems():
 
1401
            self._write_properties(indent, handler(revision))
 
1402
 
 
1403
    def _write_properties(self, indent, properties):
1474
1404
        for key, value in properties.items():
1475
 
            lines.append(key + ': ' + value)
1476
 
        return lines
 
1405
            self.to_file.write(indent + key + ': ' + value + '\n')
1477
1406
 
1478
1407
    def show_diff(self, to_file, diff, indent):
1479
1408
        for l in diff.rstrip().split('\n'):
1480
1409
            to_file.write(indent + '%s\n' % (l,))
1481
1410
 
1482
1411
 
1483
 
# Separator between revisions in long format
1484
 
_LONG_SEP = '-' * 60
1485
 
 
1486
 
 
1487
1412
class LongLogFormatter(LogFormatter):
1488
1413
 
1489
1414
    supports_merge_revisions = True
1492
1417
    supports_tags = True
1493
1418
    supports_diff = True
1494
1419
 
1495
 
    def __init__(self, *args, **kwargs):
1496
 
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1497
 
        if self.show_timezone == 'original':
1498
 
            self.date_string = self._date_string_original_timezone
1499
 
        else:
1500
 
            self.date_string = self._date_string_with_timezone
1501
 
 
1502
 
    def _date_string_with_timezone(self, rev):
1503
 
        return format_date(rev.timestamp, rev.timezone or 0,
1504
 
                           self.show_timezone)
1505
 
 
1506
 
    def _date_string_original_timezone(self, rev):
1507
 
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1508
 
            rev.timezone or 0)
1509
 
 
1510
1420
    def log_revision(self, revision):
1511
1421
        """Log a revision, either merged or not."""
1512
1422
        indent = '    ' * revision.merge_depth
1513
 
        lines = [_LONG_SEP]
 
1423
        to_file = self.to_file
 
1424
        to_file.write(indent + '-' * 60 + '\n')
1514
1425
        if revision.revno is not None:
1515
 
            lines.append('revno: %s%s' % (revision.revno,
 
1426
            to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
1516
1427
                self.merge_marker(revision)))
1517
1428
        if revision.tags:
1518
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1429
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1519
1430
        if self.show_ids:
1520
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1431
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
 
1432
            to_file.write('\n')
1521
1433
            for parent_id in revision.rev.parent_ids:
1522
 
                lines.append('parent: %s' % (parent_id,))
1523
 
        lines.extend(self.custom_properties(revision.rev))
 
1434
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
 
1435
        self.show_foreign_info(revision.rev, indent)
 
1436
        self.show_properties(revision.rev, indent)
1524
1437
 
1525
1438
        committer = revision.rev.committer
1526
1439
        authors = revision.rev.get_apparent_authors()
1527
1440
        if authors != [committer]:
1528
 
            lines.append('author: %s' % (", ".join(authors),))
1529
 
        lines.append('committer: %s' % (committer,))
 
1441
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
 
1442
        to_file.write(indent + 'committer: %s\n' % (committer,))
1530
1443
 
1531
1444
        branch_nick = revision.rev.properties.get('branch-nick', None)
1532
1445
        if branch_nick is not None:
1533
 
            lines.append('branch nick: %s' % (branch_nick,))
1534
 
 
1535
 
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1536
 
 
1537
 
        lines.append('message:')
 
1446
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
 
1447
 
 
1448
        date_str = format_date(revision.rev.timestamp,
 
1449
                               revision.rev.timezone or 0,
 
1450
                               self.show_timezone)
 
1451
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
 
1452
 
 
1453
        to_file.write(indent + 'message:\n')
1538
1454
        if not revision.rev.message:
1539
 
            lines.append('  (no message)')
 
1455
            to_file.write(indent + '  (no message)\n')
1540
1456
        else:
1541
1457
            message = revision.rev.message.rstrip('\r\n')
1542
1458
            for l in message.split('\n'):
1543
 
                lines.append('  %s' % (l,))
1544
 
 
1545
 
        # Dump the output, appending the delta and diff if requested
1546
 
        to_file = self.to_file
1547
 
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
 
1459
                to_file.write(indent + '  %s\n' % (l,))
1548
1460
        if revision.delta is not None:
1549
 
            # Use the standard status output to display changes
1550
 
            from bzrlib.delta import report_delta
1551
 
            report_delta(to_file, revision.delta, short_status=False, 
1552
 
                         show_ids=self.show_ids, indent=indent)
 
1461
            # We don't respect delta_format for compatibility
 
1462
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1463
                                short_status=False)
1553
1464
        if revision.diff is not None:
1554
1465
            to_file.write(indent + 'diff:\n')
1555
 
            to_file.flush()
1556
1466
            # Note: we explicitly don't indent the diff (relative to the
1557
1467
            # revision information) so that the output can be fed to patch -p0
1558
1468
            self.show_diff(self.to_exact_file, revision.diff, indent)
1559
 
            self.to_exact_file.flush()
1560
1469
 
1561
1470
    def get_advice_separator(self):
1562
1471
        """Get the text separating the log from the closing advice."""
1606
1515
                            self.show_timezone, date_fmt="%Y-%m-%d",
1607
1516
                            show_offset=False),
1608
1517
                tags, self.merge_marker(revision)))
 
1518
        self.show_foreign_info(revision.rev, indent+offset)
1609
1519
        self.show_properties(revision.rev, indent+offset)
1610
1520
        if self.show_ids:
1611
1521
            to_file.write(indent + offset + 'revision-id:%s\n'
1618
1528
                to_file.write(indent + offset + '%s\n' % (l,))
1619
1529
 
1620
1530
        if revision.delta is not None:
1621
 
            # Use the standard status output to display changes
1622
 
            from bzrlib.delta import report_delta
1623
 
            report_delta(to_file, revision.delta, 
1624
 
                         short_status=self.delta_format==1, 
1625
 
                         show_ids=self.show_ids, indent=indent + offset)
 
1531
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1532
                                short_status=self.delta_format==1)
1626
1533
        if revision.diff is not None:
1627
1534
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1628
1535
        to_file.write('\n')
1636
1543
 
1637
1544
    def __init__(self, *args, **kwargs):
1638
1545
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1639
 
        width = terminal_width()
1640
 
        if width is not None:
1641
 
            # we need one extra space for terminals that wrap on last char
1642
 
            width = width - 1
1643
 
        self._max_chars = width
 
1546
        self._max_chars = terminal_width() - 1
1644
1547
 
1645
1548
    def truncate(self, str, max_len):
1646
 
        if max_len is None or len(str) <= max_len:
 
1549
        if len(str) <= max_len:
1647
1550
            return str
1648
 
        return str[:max_len-3] + '...'
 
1551
        return str[:max_len-3]+'...'
1649
1552
 
1650
1553
    def date_string(self, rev):
1651
1554
        return format_date(rev.timestamp, rev.timezone or 0,
1703
1606
                               self.show_timezone,
1704
1607
                               date_fmt='%Y-%m-%d',
1705
1608
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
 
1609
        committer_str = revision.rev.committer.replace (' <', '  <')
1707
1610
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1708
1611
 
1709
1612
        if revision.delta is not None and revision.delta.has_changed():
1943
1846
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
1847
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
1848
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
 
      branch will be read-locked.
1947
1849
    """
1948
1850
    from builtins import _get_revision_range, safe_relpath_files
1949
1851
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
1951
1852
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
1853
    # those paths might be deleted ones, they might be on a case-insensitive
1953
1854
    # filesystem and/or they might be in silly locations (like another branch).
2032
1933
 
2033
1934
properties_handler_registry = registry.Registry()
2034
1935
 
2035
 
# Use the properties handlers to print out bug information if available
2036
 
def _bugs_properties_handler(revision):
2037
 
    if revision.properties.has_key('bugs'):
2038
 
        bug_lines = revision.properties['bugs'].split('\n')
2039
 
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2040
 
        fixed_bug_urls = [row[0] for row in bug_rows if
2041
 
                          len(row) > 1 and row[1] == 'fixed']
2042
 
 
2043
 
        if fixed_bug_urls:
2044
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2045
 
    return {}
2046
 
 
2047
 
properties_handler_registry.register('bugs_properties_handler',
2048
 
                                     _bugs_properties_handler)
2049
 
 
2050
1936
 
2051
1937
# adapters which revision ids to log are filtered. When log is called, the
2052
1938
# log_rev_iterator is adapted through each of these factory methods.