~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Vincent Ladeuil
  • Date: 2010-04-23 08:51:52 UTC
  • mfrom: (5131.2.6 support_OO_flag)
  • mto: This revision was merged to the branch mainline in revision 5179.
  • Revision ID: v.ladeuil+lp@free.fr-20100423085152-uoewc1vnkwqhw0pj
Manually assign docstrings to command objects, so that they work with python -OO

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
69
69
    config,
70
70
    diff,
71
71
    errors,
 
72
    foreign,
72
73
    repository as _mod_repository,
73
74
    revision as _mod_revision,
74
75
    revisionspec,
82
83
    )
83
84
from bzrlib.osutils import (
84
85
    format_date,
 
86
    format_date_with_offset_in_original_timezone,
85
87
    get_terminal_encoding,
86
88
    re_compile_checked,
87
89
    terminal_width,
88
90
    )
 
91
from bzrlib.symbol_versioning import (
 
92
    deprecated_function,
 
93
    deprecated_in,
 
94
    )
89
95
 
90
96
 
91
97
def find_touching_revisions(branch, file_id):
103
109
    last_path = None
104
110
    revno = 1
105
111
    for revision_id in branch.revision_history():
106
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
 
112
        this_inv = branch.repository.get_inventory(revision_id)
107
113
        if file_id in this_inv:
108
114
            this_ie = this_inv[file_id]
109
115
            this_path = this_inv.id2path(file_id)
279
285
        'diff_type': diff_type,
280
286
        # Add 'private' attributes for features that may be deprecated
281
287
        '_match_using_deltas': _match_using_deltas,
282
 
        '_allow_single_merge_revision': True,
283
288
    }
284
289
 
285
290
 
303
308
 
304
309
 
305
310
class Logger(object):
306
 
    """An object the generates, formats and displays a log."""
 
311
    """An object that generates, formats and displays a log."""
307
312
 
308
313
    def __init__(self, branch, rqst):
309
314
        """Create a Logger.
348
353
            rqst['delta_type'] = None
349
354
        if not getattr(lf, 'supports_diff', False):
350
355
            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)
354
356
 
355
357
        # Find and print the interesting revisions
356
358
        generator = self._generator_factory(self.branch, rqst)
357
359
        for lr in generator.iter_log_revisions():
358
360
            lf.log_revision(lr)
 
361
        lf.show_advice()
359
362
 
360
363
    def _generator_factory(self, branch, rqst):
361
364
        """Make the LogGenerator object to use.
386
389
        :return: An iterator yielding LogRevision objects.
387
390
        """
388
391
        rqst = self.rqst
 
392
        levels = rqst.get('levels')
 
393
        limit = rqst.get('limit')
 
394
        diff_type = rqst.get('diff_type')
389
395
        log_count = 0
390
396
        revision_iterator = self._create_log_revision_iterator()
391
397
        for revs in revision_iterator:
392
398
            for (rev_id, revno, merge_depth), rev, delta in revs:
393
399
                # 0 levels means show everything; merge_depth counts from 0
394
 
                levels = rqst.get('levels')
395
400
                if levels != 0 and merge_depth >= levels:
396
401
                    continue
397
 
                diff = self._format_diff(rev, rev_id)
 
402
                if diff_type is None:
 
403
                    diff = None
 
404
                else:
 
405
                    diff = self._format_diff(rev, rev_id, diff_type)
398
406
                yield LogRevision(rev, revno, merge_depth, delta,
399
407
                    self.rev_tag_dict.get(rev_id), diff)
400
 
                limit = rqst.get('limit')
401
408
                if limit:
402
409
                    log_count += 1
403
410
                    if log_count >= limit:
404
411
                        return
405
412
 
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
 
413
    def _format_diff(self, rev, rev_id, diff_type):
410
414
        repo = self.branch.repository
411
415
        if len(rev.parent_ids) == 0:
412
416
            ancestor_id = _mod_revision.NULL_REVISION
451
455
        generate_merge_revisions = rqst.get('levels') != 1
452
456
        delayed_graph_generation = not rqst.get('specific_fileids') and (
453
457
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
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'),
 
458
        view_revisions = _calc_view_revisions(
 
459
            self.branch, self.start_rev_id, self.end_rev_id,
 
460
            rqst.get('direction'),
 
461
            generate_merge_revisions=generate_merge_revisions,
457
462
            delayed_graph_generation=delayed_graph_generation)
458
463
 
459
464
        # Apply the other filters
467
472
        # Note that we always generate the merge revisions because
468
473
        # filter_revisions_touching_file_id() requires them ...
469
474
        rqst = self.rqst
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'))
 
475
        view_revisions = _calc_view_revisions(
 
476
            self.branch, self.start_rev_id, self.end_rev_id,
 
477
            rqst.get('direction'), generate_merge_revisions=True)
473
478
        if not isinstance(view_revisions, list):
474
479
            view_revisions = list(view_revisions)
475
480
        view_revisions = _filter_revisions_touching_file_id(self.branch,
480
485
 
481
486
 
482
487
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
483
 
    generate_merge_revisions, allow_single_merge_revision,
484
 
    delayed_graph_generation=False):
 
488
    generate_merge_revisions, delayed_graph_generation=False):
485
489
    """Calculate the revisions to view.
486
490
 
487
491
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
488
492
             a list of the same tuples.
489
493
    """
 
494
    if direction not in ('reverse', 'forward'):
 
495
        raise ValueError('invalid direction %r' % direction)
490
496
    br_revno, br_rev_id = branch.last_revision_info()
491
497
    if br_revno == 0:
492
498
        return []
493
499
 
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)
 
500
    if (end_rev_id and start_rev_id == end_rev_id
 
501
        and (not generate_merge_revisions
 
502
             or not _has_merges(branch, end_rev_id))):
 
503
        # If a single revision is requested, check we can handle it
 
504
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
 
505
                                           br_revno)
 
506
    elif not generate_merge_revisions:
 
507
        # If we only want to see linear revisions, we can iterate ...
 
508
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
509
                                             direction)
 
510
        if direction == 'forward':
 
511
            iter_revs = reversed(iter_revs)
505
512
    else:
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):
 
513
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
514
                                            direction, delayed_graph_generation)
 
515
        if direction == 'forward':
 
516
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
517
    return iter_revs
 
518
 
 
519
 
 
520
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
512
521
    if rev_id == br_rev_id:
513
522
        # It's the tip
514
523
        return [(br_rev_id, br_revno, 0)]
515
524
    else:
516
525
        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.')
524
526
        revno_str = '.'.join(str(n) for n in revno)
525
527
        return [(rev_id, revno_str, 0)]
526
528
 
536
538
        except _StartNotLinearAncestor:
537
539
            raise errors.BzrCommandError('Start revision not found in'
538
540
                ' left-hand history of end revision.')
539
 
    if direction == 'forward':
540
 
        result = reversed(result)
541
541
    return result
542
542
 
543
543
 
544
544
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
545
 
    delayed_graph_generation):
 
545
                            delayed_graph_generation):
546
546
    # On large trees, generating the merge graph can take 30-60 seconds
547
547
    # so we delay doing it until a merge is detected, incrementally
548
548
    # returning initial (non-merge) revisions while we can.
 
549
 
 
550
    # The above is only true for old formats (<= 0.92), for newer formats, a
 
551
    # couple of seconds only should be needed to load the whole graph and the
 
552
    # other graph operations needed are even faster than that -- vila 100201
549
553
    initial_revisions = []
550
554
    if delayed_graph_generation:
551
555
        try:
552
 
            for rev_id, revno, depth in \
553
 
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
556
            for rev_id, revno, depth in  _linear_view_revisions(
 
557
                branch, start_rev_id, end_rev_id):
554
558
                if _has_merges(branch, rev_id):
 
559
                    # The end_rev_id can be nested down somewhere. We need an
 
560
                    # explicit ancestry check. There is an ambiguity here as we
 
561
                    # may not raise _StartNotLinearAncestor for a revision that
 
562
                    # is an ancestor but not a *linear* one. But since we have
 
563
                    # loaded the graph to do the check (or calculate a dotted
 
564
                    # revno), we may as well accept to show the log...  We need
 
565
                    # the check only if start_rev_id is not None as all
 
566
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
 
567
                    # -- vila 20100319
 
568
                    graph = branch.repository.get_graph()
 
569
                    if (start_rev_id is not None
 
570
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
571
                        raise _StartNotLinearAncestor()
 
572
                    # Since we collected the revisions so far, we need to
 
573
                    # adjust end_rev_id.
555
574
                    end_rev_id = rev_id
556
575
                    break
557
576
                else:
558
577
                    initial_revisions.append((rev_id, revno, depth))
559
578
            else:
560
579
                # No merged revisions found
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)
 
580
                return initial_revisions
567
581
        except _StartNotLinearAncestor:
568
582
            # A merge was never detected so the lower revision limit can't
569
583
            # be nested down somewhere
570
584
            raise errors.BzrCommandError('Start revision not found in'
571
585
                ' history of end revision.')
572
586
 
 
587
    # We exit the loop above because we encounter a revision with merges, from
 
588
    # this revision, we need to switch to _graph_view_revisions.
 
589
 
573
590
    # A log including nested merges is required. If the direction is reverse,
574
591
    # we rebase the initial merge depths so that the development line is
575
592
    # shown naturally, i.e. just like it is for linear logging. We can easily
577
594
    # indented at the end seems slightly nicer in that case.
578
595
    view_revisions = chain(iter(initial_revisions),
579
596
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
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)
 
597
                              rebase_initial_depths=(direction == 'reverse')))
 
598
    return view_revisions
589
599
 
590
600
 
591
601
def _has_merges(branch, rev_id):
609
619
        else:
610
620
            # not obvious
611
621
            return False
 
622
    # if either start or end is not specified then we use either the first or
 
623
    # the last revision and *they* are obvious ancestors.
612
624
    return True
613
625
 
614
626
 
647
659
 
648
660
 
649
661
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
650
 
    rebase_initial_depths=True):
 
662
                          rebase_initial_depths=True):
651
663
    """Calculate revisions to view including merges, newest to oldest.
652
664
 
653
665
    :param branch: the branch
676
688
                depth_adjustment = merge_depth
677
689
            if depth_adjustment:
678
690
                if merge_depth < depth_adjustment:
 
691
                    # From now on we reduce the depth adjustement, this can be
 
692
                    # surprising for users. The alternative requires two passes
 
693
                    # which breaks the fast display of the first revision
 
694
                    # though.
679
695
                    depth_adjustment = merge_depth
680
696
                merge_depth -= depth_adjustment
681
697
            yield rev_id, '.'.join(map(str, revno)), merge_depth
682
698
 
683
699
 
 
700
@deprecated_function(deprecated_in((2, 2, 0)))
684
701
def calculate_view_revisions(branch, start_revision, end_revision, direction,
685
 
        specific_fileid, generate_merge_revisions, allow_single_merge_revision):
 
702
        specific_fileid, generate_merge_revisions):
686
703
    """Calculate the revisions to view.
687
704
 
688
705
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
689
706
             a list of the same tuples.
690
707
    """
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
694
708
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
695
709
        end_revision)
696
710
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
697
 
        direction, generate_merge_revisions or specific_fileid,
698
 
        allow_single_merge_revision))
 
711
        direction, generate_merge_revisions or specific_fileid))
699
712
    if specific_fileid:
700
713
        view_revisions = _filter_revisions_touching_file_id(branch,
701
714
            specific_fileid, view_revisions,
1047
1060
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1048
1061
 
1049
1062
 
 
1063
@deprecated_function(deprecated_in((2, 2, 0)))
1050
1064
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1051
1065
    """Filter view_revisions based on revision ranges.
1052
1066
 
1061
1075
 
1062
1076
    :return: The filtered view_revisions.
1063
1077
    """
1064
 
    # This method is no longer called by the main code path.
1065
 
    # It may be removed soon. IGC 20090127
1066
1078
    if start_rev_id or end_rev_id:
1067
1079
        revision_ids = [r for r, n, d in view_revisions]
1068
1080
        if start_rev_id:
1174
1186
    return result
1175
1187
 
1176
1188
 
 
1189
@deprecated_function(deprecated_in((2, 2, 0)))
1177
1190
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1178
1191
                       include_merges=True):
1179
1192
    """Produce an iterator of revisions to show
1180
1193
    :return: an iterator of (revision_id, revno, merge_depth)
1181
1194
    (if there is no revno for a revision, None is supplied)
1182
1195
    """
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
1186
1196
    if not include_merges:
1187
1197
        revision_ids = mainline_revs[1:]
1188
1198
        if direction == 'reverse':
1283
1293
        one (2) should be used.
1284
1294
 
1285
1295
    - supports_merge_revisions must be True if this log formatter supports
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.
 
1296
        merge revisions.  If not, then only mainline revisions will be passed
 
1297
        to the formatter.
1289
1298
 
1290
1299
    - preferred_levels is the number of levels this formatter defaults to.
1291
1300
        The default value is zero meaning display all levels.
1292
1301
        This value is only relevant if supports_merge_revisions is True.
1293
1302
 
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
 
 
1298
1303
    - supports_tags must be True if this log formatter supports tags.
1299
1304
        Otherwise the tags attribute may not be populated.
1300
1305
 
1311
1316
    preferred_levels = 0
1312
1317
 
1313
1318
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1314
 
                 delta_format=None, levels=None):
 
1319
                 delta_format=None, levels=None, show_advice=False,
 
1320
                 to_exact_file=None):
1315
1321
        """Create a LogFormatter.
1316
1322
 
1317
1323
        :param to_file: the file to output to
 
1324
        :param to_exact_file: if set, gives an output stream to which 
 
1325
             non-Unicode diffs are written.
1318
1326
        :param show_ids: if True, revision-ids are to be displayed
1319
1327
        :param show_timezone: the timezone to use
1320
1328
        :param delta_format: the level of delta information to display
1321
 
          or None to leave it u to the formatter to decide
 
1329
          or None to leave it to the formatter to decide
1322
1330
        :param levels: the number of levels to display; None or -1 to
1323
1331
          let the log formatter decide.
 
1332
        :param show_advice: whether to show advice at the end of the
 
1333
          log or not
1324
1334
        """
1325
1335
        self.to_file = to_file
1326
1336
        # 'exact' stream used to show diff, it should print content 'as is'
1327
1337
        # and should not try to decode/encode it to unicode to avoid bug #328007
1328
 
        self.to_exact_file = getattr(to_file, 'stream', to_file)
 
1338
        if to_exact_file is not None:
 
1339
            self.to_exact_file = to_exact_file
 
1340
        else:
 
1341
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1342
            # for code that expects to get diffs to pass in the exact file
 
1343
            # stream
 
1344
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1329
1345
        self.show_ids = show_ids
1330
1346
        self.show_timezone = show_timezone
1331
1347
        if delta_format is None:
1333
1349
            delta_format = 2 # long format
1334
1350
        self.delta_format = delta_format
1335
1351
        self.levels = levels
 
1352
        self._show_advice = show_advice
 
1353
        self._merge_count = 0
1336
1354
 
1337
1355
    def get_levels(self):
1338
1356
        """Get the number of levels to display or 0 for all."""
1339
1357
        if getattr(self, 'supports_merge_revisions', False):
1340
1358
            if self.levels is None or self.levels == -1:
1341
 
                return self.preferred_levels
1342
 
            else:
1343
 
                return self.levels
1344
 
        return 1
 
1359
                self.levels = self.preferred_levels
 
1360
        else:
 
1361
            self.levels = 1
 
1362
        return self.levels
1345
1363
 
1346
1364
    def log_revision(self, revision):
1347
1365
        """Log a revision.
1350
1368
        """
1351
1369
        raise NotImplementedError('not implemented in abstract base')
1352
1370
 
 
1371
    def show_advice(self):
 
1372
        """Output user advice, if any, when the log is completed."""
 
1373
        if self._show_advice and self.levels == 1 and self._merge_count > 0:
 
1374
            advice_sep = self.get_advice_separator()
 
1375
            if advice_sep:
 
1376
                self.to_file.write(advice_sep)
 
1377
            self.to_file.write(
 
1378
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1379
 
 
1380
    def get_advice_separator(self):
 
1381
        """Get the text separating the log from the closing advice."""
 
1382
        return ''
 
1383
 
1353
1384
    def short_committer(self, rev):
1354
1385
        name, address = config.parse_username(rev.committer)
1355
1386
        if name:
1362
1393
            return name
1363
1394
        return address
1364
1395
 
 
1396
    def merge_marker(self, revision):
 
1397
        """Get the merge marker to include in the output or '' if none."""
 
1398
        if len(revision.rev.parent_ids) > 1:
 
1399
            self._merge_count += 1
 
1400
            return ' [merge]'
 
1401
        else:
 
1402
            return ''
 
1403
 
1365
1404
    def show_properties(self, revision, indent):
1366
1405
        """Displays the custom properties returned by each registered handler.
1367
1406
 
1368
1407
        If a registered handler raises an error it is propagated.
1369
1408
        """
 
1409
        for line in self.custom_properties(revision):
 
1410
            self.to_file.write("%s%s\n" % (indent, line))
 
1411
 
 
1412
    def custom_properties(self, revision):
 
1413
        """Format the custom properties returned by each registered handler.
 
1414
 
 
1415
        If a registered handler raises an error it is propagated.
 
1416
 
 
1417
        :return: a list of formatted lines (excluding trailing newlines)
 
1418
        """
 
1419
        lines = self._foreign_info_properties(revision)
1370
1420
        for key, handler in properties_handler_registry.iteritems():
1371
 
            for key, value in handler(revision).items():
1372
 
                self.to_file.write(indent + key + ': ' + value + '\n')
 
1421
            lines.extend(self._format_properties(handler(revision)))
 
1422
        return lines
 
1423
 
 
1424
    def _foreign_info_properties(self, rev):
 
1425
        """Custom log displayer for foreign revision identifiers.
 
1426
 
 
1427
        :param rev: Revision object.
 
1428
        """
 
1429
        # Revision comes directly from a foreign repository
 
1430
        if isinstance(rev, foreign.ForeignRevision):
 
1431
            return self._format_properties(
 
1432
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1433
 
 
1434
        # Imported foreign revision revision ids always contain :
 
1435
        if not ":" in rev.revision_id:
 
1436
            return []
 
1437
 
 
1438
        # Revision was once imported from a foreign repository
 
1439
        try:
 
1440
            foreign_revid, mapping = \
 
1441
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
 
1442
        except errors.InvalidRevisionId:
 
1443
            return []
 
1444
 
 
1445
        return self._format_properties(
 
1446
            mapping.vcs.show_foreign_revid(foreign_revid))
 
1447
 
 
1448
    def _format_properties(self, properties):
 
1449
        lines = []
 
1450
        for key, value in properties.items():
 
1451
            lines.append(key + ': ' + value)
 
1452
        return lines
1373
1453
 
1374
1454
    def show_diff(self, to_file, diff, indent):
1375
1455
        for l in diff.rstrip().split('\n'):
1376
1456
            to_file.write(indent + '%s\n' % (l,))
1377
1457
 
1378
1458
 
 
1459
# Separator between revisions in long format
 
1460
_LONG_SEP = '-' * 60
 
1461
 
 
1462
 
1379
1463
class LongLogFormatter(LogFormatter):
1380
1464
 
1381
1465
    supports_merge_revisions = True
 
1466
    preferred_levels = 1
1382
1467
    supports_delta = True
1383
1468
    supports_tags = True
1384
1469
    supports_diff = True
1385
1470
 
 
1471
    def __init__(self, *args, **kwargs):
 
1472
        super(LongLogFormatter, self).__init__(*args, **kwargs)
 
1473
        if self.show_timezone == 'original':
 
1474
            self.date_string = self._date_string_original_timezone
 
1475
        else:
 
1476
            self.date_string = self._date_string_with_timezone
 
1477
 
 
1478
    def _date_string_with_timezone(self, rev):
 
1479
        return format_date(rev.timestamp, rev.timezone or 0,
 
1480
                           self.show_timezone)
 
1481
 
 
1482
    def _date_string_original_timezone(self, rev):
 
1483
        return format_date_with_offset_in_original_timezone(rev.timestamp,
 
1484
            rev.timezone or 0)
 
1485
 
1386
1486
    def log_revision(self, revision):
1387
1487
        """Log a revision, either merged or not."""
1388
1488
        indent = '    ' * revision.merge_depth
1389
 
        to_file = self.to_file
1390
 
        to_file.write(indent + '-' * 60 + '\n')
 
1489
        lines = [_LONG_SEP]
1391
1490
        if revision.revno is not None:
1392
 
            to_file.write(indent + 'revno: %s\n' % (revision.revno,))
 
1491
            lines.append('revno: %s%s' % (revision.revno,
 
1492
                self.merge_marker(revision)))
1393
1493
        if revision.tags:
1394
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
 
1494
            lines.append('tags: %s' % (', '.join(revision.tags)))
1395
1495
        if self.show_ids:
1396
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1397
 
            to_file.write('\n')
 
1496
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1398
1497
            for parent_id in revision.rev.parent_ids:
1399
 
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
1400
 
        self.show_properties(revision.rev, indent)
 
1498
                lines.append('parent: %s' % (parent_id,))
 
1499
        lines.extend(self.custom_properties(revision.rev))
1401
1500
 
1402
1501
        committer = revision.rev.committer
1403
1502
        authors = revision.rev.get_apparent_authors()
1404
1503
        if authors != [committer]:
1405
 
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1406
 
        to_file.write(indent + 'committer: %s\n' % (committer,))
 
1504
            lines.append('author: %s' % (", ".join(authors),))
 
1505
        lines.append('committer: %s' % (committer,))
1407
1506
 
1408
1507
        branch_nick = revision.rev.properties.get('branch-nick', None)
1409
1508
        if branch_nick is not None:
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')
 
1509
            lines.append('branch nick: %s' % (branch_nick,))
 
1510
 
 
1511
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1512
 
 
1513
        lines.append('message:')
1418
1514
        if not revision.rev.message:
1419
 
            to_file.write(indent + '  (no message)\n')
 
1515
            lines.append('  (no message)')
1420
1516
        else:
1421
1517
            message = revision.rev.message.rstrip('\r\n')
1422
1518
            for l in message.split('\n'):
1423
 
                to_file.write(indent + '  %s\n' % (l,))
 
1519
                lines.append('  %s' % (l,))
 
1520
 
 
1521
        # Dump the output, appending the delta and diff if requested
 
1522
        to_file = self.to_file
 
1523
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1424
1524
        if revision.delta is not None:
1425
 
            # We don't respect delta_format for compatibility
1426
 
            revision.delta.show(to_file, self.show_ids, indent=indent,
1427
 
                                short_status=False)
 
1525
            # Use the standard status output to display changes
 
1526
            from bzrlib.delta import report_delta
 
1527
            report_delta(to_file, revision.delta, short_status=False, 
 
1528
                         show_ids=self.show_ids, indent=indent)
1428
1529
        if revision.diff is not None:
1429
1530
            to_file.write(indent + 'diff:\n')
 
1531
            to_file.flush()
1430
1532
            # Note: we explicitly don't indent the diff (relative to the
1431
1533
            # revision information) so that the output can be fed to patch -p0
1432
1534
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1535
            self.to_exact_file.flush()
 
1536
 
 
1537
    def get_advice_separator(self):
 
1538
        """Get the text separating the log from the closing advice."""
 
1539
        return '-' * 60 + '\n'
1433
1540
 
1434
1541
 
1435
1542
class ShortLogFormatter(LogFormatter):
1465
1572
        offset = ' ' * (revno_width + 1)
1466
1573
 
1467
1574
        to_file = self.to_file
1468
 
        is_merge = ''
1469
 
        if len(revision.rev.parent_ids) > 1:
1470
 
            is_merge = ' [merge]'
1471
1575
        tags = ''
1472
1576
        if revision.tags:
1473
1577
            tags = ' {%s}' % (', '.join(revision.tags))
1477
1581
                            revision.rev.timezone or 0,
1478
1582
                            self.show_timezone, date_fmt="%Y-%m-%d",
1479
1583
                            show_offset=False),
1480
 
                tags, is_merge))
 
1584
                tags, self.merge_marker(revision)))
1481
1585
        self.show_properties(revision.rev, indent+offset)
1482
1586
        if self.show_ids:
1483
1587
            to_file.write(indent + offset + 'revision-id:%s\n'
1490
1594
                to_file.write(indent + offset + '%s\n' % (l,))
1491
1595
 
1492
1596
        if revision.delta is not None:
1493
 
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
1494
 
                                short_status=self.delta_format==1)
 
1597
            # Use the standard status output to display changes
 
1598
            from bzrlib.delta import report_delta
 
1599
            report_delta(to_file, revision.delta, 
 
1600
                         short_status=self.delta_format==1, 
 
1601
                         show_ids=self.show_ids, indent=indent + offset)
1495
1602
        if revision.diff is not None:
1496
1603
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1497
1604
        to_file.write('\n')
1505
1612
 
1506
1613
    def __init__(self, *args, **kwargs):
1507
1614
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1508
 
        self._max_chars = terminal_width() - 1
 
1615
        width = terminal_width()
 
1616
        if width is not None:
 
1617
            # we need one extra space for terminals that wrap on last char
 
1618
            width = width - 1
 
1619
        self._max_chars = width
1509
1620
 
1510
1621
    def truncate(self, str, max_len):
1511
 
        if len(str) <= max_len:
 
1622
        if max_len is None or len(str) <= max_len:
1512
1623
            return str
1513
 
        return str[:max_len-3]+'...'
 
1624
        return str[:max_len-3] + '...'
1514
1625
 
1515
1626
    def date_string(self, rev):
1516
1627
        return format_date(rev.timestamp, rev.timezone or 0,
1568
1679
                               self.show_timezone,
1569
1680
                               date_fmt='%Y-%m-%d',
1570
1681
                               show_offset=False)
1571
 
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1682
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
1572
1683
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1573
1684
 
1574
1685
        if revision.delta is not None and revision.delta.has_changed():
1808
1919
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1809
1920
      info_list is a list of (relative_path, file_id, kind) tuples where
1810
1921
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
1922
      branch will be read-locked.
1811
1923
    """
1812
1924
    from builtins import _get_revision_range, safe_relpath_files
1813
1925
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
1926
    b.lock_read()
1814
1927
    # XXX: It's damn messy converting a list of paths to relative paths when
1815
1928
    # those paths might be deleted ones, they might be on a case-insensitive
1816
1929
    # filesystem and/or they might be in silly locations (like another branch).
1826
1939
    info_list = []
1827
1940
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
1828
1941
        "log")
 
1942
    if relpaths in ([], [u'']):
 
1943
        return b, [], start_rev_info, end_rev_info
1829
1944
    if start_rev_info is None and end_rev_info is None:
1830
1945
        if tree is None:
1831
1946
            tree = b.basis_tree()
1892
2007
 
1893
2008
 
1894
2009
properties_handler_registry = registry.Registry()
1895
 
properties_handler_registry.register_lazy("foreign",
1896
 
                                          "bzrlib.foreign",
1897
 
                                          "show_foreign_properties")
 
2010
 
 
2011
# Use the properties handlers to print out bug information if available
 
2012
def _bugs_properties_handler(revision):
 
2013
    if revision.properties.has_key('bugs'):
 
2014
        bug_lines = revision.properties['bugs'].split('\n')
 
2015
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2016
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2017
                          len(row) > 1 and row[1] == 'fixed']
 
2018
 
 
2019
        if fixed_bug_urls:
 
2020
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2021
    return {}
 
2022
 
 
2023
properties_handler_registry.register('bugs_properties_handler',
 
2024
                                     _bugs_properties_handler)
1898
2025
 
1899
2026
 
1900
2027
# adapters which revision ids to log are filtered. When log is called, the