~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Martin
  • Date: 2010-05-16 15:18:43 UTC
  • mfrom: (5235 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5239.
  • Revision ID: gzlist@googlemail.com-20100516151843-lu53u7caehm3ie3i
Merge bzr.dev to resolve conflicts in NEWS and _chk_map_pyx

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)
214
220
    'direction': 'reverse',
215
221
    'levels': 1,
216
222
    'generate_tags': True,
 
223
    'exclude_common_ancestry': False,
217
224
    '_match_using_deltas': True,
218
225
    }
219
226
 
220
227
 
221
228
def make_log_request_dict(direction='reverse', specific_fileids=None,
222
 
    start_revision=None, end_revision=None, limit=None,
223
 
    message_search=None, levels=1, generate_tags=True, delta_type=None,
224
 
    diff_type=None, _match_using_deltas=True):
 
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
                          ):
225
235
    """Convenience function for making a logging request dictionary.
226
236
 
227
237
    Using this function may make code slightly safer by ensuring
265
275
      algorithm used for matching specific_fileids. This parameter
266
276
      may be removed in the future so bzrlib client code should NOT
267
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.
268
281
    """
269
282
    return {
270
283
        'direction': direction,
277
290
        'generate_tags': generate_tags,
278
291
        'delta_type': delta_type,
279
292
        'diff_type': diff_type,
 
293
        'exclude_common_ancestry': exclude_common_ancestry,
280
294
        # Add 'private' attributes for features that may be deprecated
281
295
        '_match_using_deltas': _match_using_deltas,
282
 
        '_allow_single_merge_revision': True,
283
296
    }
284
297
 
285
298
 
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.
348
361
            rqst['delta_type'] = None
349
362
        if not getattr(lf, 'supports_diff', False):
350
363
            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
364
 
355
365
        # Find and print the interesting revisions
356
366
        generator = self._generator_factory(self.branch, rqst)
357
367
        for lr in generator.iter_log_revisions():
358
368
            lf.log_revision(lr)
 
369
        lf.show_advice()
359
370
 
360
371
    def _generator_factory(self, branch, rqst):
361
372
        """Make the LogGenerator object to use.
386
397
        :return: An iterator yielding LogRevision objects.
387
398
        """
388
399
        rqst = self.rqst
 
400
        levels = rqst.get('levels')
 
401
        limit = rqst.get('limit')
 
402
        diff_type = rqst.get('diff_type')
389
403
        log_count = 0
390
404
        revision_iterator = self._create_log_revision_iterator()
391
405
        for revs in revision_iterator:
392
406
            for (rev_id, revno, merge_depth), rev, delta in revs:
393
407
                # 0 levels means show everything; merge_depth counts from 0
394
 
                levels = rqst.get('levels')
395
408
                if levels != 0 and merge_depth >= levels:
396
409
                    continue
397
 
                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)
398
414
                yield LogRevision(rev, revno, merge_depth, delta,
399
415
                    self.rev_tag_dict.get(rev_id), diff)
400
 
                limit = rqst.get('limit')
401
416
                if limit:
402
417
                    log_count += 1
403
418
                    if log_count >= limit:
404
419
                        return
405
420
 
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
 
421
    def _format_diff(self, rev, rev_id, diff_type):
410
422
        repo = self.branch.repository
411
423
        if len(rev.parent_ids) == 0:
412
424
            ancestor_id = _mod_revision.NULL_REVISION
451
463
        generate_merge_revisions = rqst.get('levels') != 1
452
464
        delayed_graph_generation = not rqst.get('specific_fileids') and (
453
465
                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'),
457
 
            delayed_graph_generation=delayed_graph_generation)
 
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'))
458
472
 
459
473
        # Apply the other filters
460
474
        return make_log_rev_iterator(self.branch, view_revisions,
467
481
        # Note that we always generate the merge revisions because
468
482
        # filter_revisions_touching_file_id() requires them ...
469
483
        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'))
 
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'))
473
488
        if not isinstance(view_revisions, list):
474
489
            view_revisions = list(view_revisions)
475
490
        view_revisions = _filter_revisions_touching_file_id(self.branch,
480
495
 
481
496
 
482
497
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):
 
498
                         generate_merge_revisions,
 
499
                         delayed_graph_generation=False,
 
500
                         exclude_common_ancestry=False,
 
501
                         ):
485
502
    """Calculate the revisions to view.
486
503
 
487
504
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
488
505
             a list of the same tuples.
489
506
    """
 
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)
490
512
    br_revno, br_rev_id = branch.last_revision_info()
491
513
    if br_revno == 0:
492
514
        return []
493
515
 
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)
 
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)
505
528
    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):
 
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
 
535
 
 
536
 
 
537
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
512
538
    if rev_id == br_rev_id:
513
539
        # It's the tip
514
540
        return [(br_rev_id, br_revno, 0)]
515
541
    else:
516
542
        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
543
        revno_str = '.'.join(str(n) for n in revno)
525
544
        return [(rev_id, revno_str, 0)]
526
545
 
536
555
        except _StartNotLinearAncestor:
537
556
            raise errors.BzrCommandError('Start revision not found in'
538
557
                ' left-hand history of end revision.')
539
 
    if direction == 'forward':
540
 
        result = reversed(result)
541
558
    return result
542
559
 
543
560
 
544
561
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
545
 
    delayed_graph_generation):
 
562
                            delayed_graph_generation,
 
563
                            exclude_common_ancestry=False):
546
564
    # On large trees, generating the merge graph can take 30-60 seconds
547
565
    # so we delay doing it until a merge is detected, incrementally
548
566
    # 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
549
571
    initial_revisions = []
550
572
    if delayed_graph_generation:
551
573
        try:
552
 
            for rev_id, revno, depth in \
553
 
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
574
            for rev_id, revno, depth in  _linear_view_revisions(
 
575
                branch, start_rev_id, end_rev_id):
554
576
                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.
555
592
                    end_rev_id = rev_id
556
593
                    break
557
594
                else:
558
595
                    initial_revisions.append((rev_id, revno, depth))
559
596
            else:
560
597
                # 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)
 
598
                return initial_revisions
567
599
        except _StartNotLinearAncestor:
568
600
            # A merge was never detected so the lower revision limit can't
569
601
            # be nested down somewhere
570
602
            raise errors.BzrCommandError('Start revision not found in'
571
603
                ' history of end revision.')
572
604
 
 
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
 
573
608
    # A log including nested merges is required. If the direction is reverse,
574
609
    # we rebase the initial merge depths so that the development line is
575
610
    # shown naturally, i.e. just like it is for linear logging. We can easily
577
612
    # indented at the end seems slightly nicer in that case.
578
613
    view_revisions = chain(iter(initial_revisions),
579
614
        _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)
 
615
                              rebase_initial_depths=(direction == 'reverse'),
 
616
                              exclude_common_ancestry=exclude_common_ancestry))
 
617
    return view_revisions
589
618
 
590
619
 
591
620
def _has_merges(branch, rev_id):
609
638
        else:
610
639
            # not obvious
611
640
            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.
612
643
    return True
613
644
 
614
645
 
647
678
 
648
679
 
649
680
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
650
 
    rebase_initial_depths=True):
 
681
                          rebase_initial_depths=True,
 
682
                          exclude_common_ancestry=False):
651
683
    """Calculate revisions to view including merges, newest to oldest.
652
684
 
653
685
    :param branch: the branch
657
689
      revision is found?
658
690
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
659
691
    """
 
692
    if exclude_common_ancestry:
 
693
        stop_rule = 'with-merges-without-common-ancestry'
 
694
    else:
 
695
        stop_rule = 'with-merges'
660
696
    view_revisions = branch.iter_merge_sorted_revisions(
661
697
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
662
 
        stop_rule="with-merges")
 
698
        stop_rule=stop_rule)
663
699
    if not rebase_initial_depths:
664
700
        for (rev_id, merge_depth, revno, end_of_merge
665
701
             ) in view_revisions:
676
712
                depth_adjustment = merge_depth
677
713
            if depth_adjustment:
678
714
                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.
679
719
                    depth_adjustment = merge_depth
680
720
                merge_depth -= depth_adjustment
681
721
            yield rev_id, '.'.join(map(str, revno)), merge_depth
682
722
 
683
723
 
 
724
@deprecated_function(deprecated_in((2, 2, 0)))
684
725
def calculate_view_revisions(branch, start_revision, end_revision, direction,
685
 
        specific_fileid, generate_merge_revisions, allow_single_merge_revision):
 
726
        specific_fileid, generate_merge_revisions):
686
727
    """Calculate the revisions to view.
687
728
 
688
729
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
689
730
             a list of the same tuples.
690
731
    """
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
732
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
695
733
        end_revision)
696
734
    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))
 
735
        direction, generate_merge_revisions or specific_fileid))
699
736
    if specific_fileid:
700
737
        view_revisions = _filter_revisions_touching_file_id(branch,
701
738
            specific_fileid, view_revisions,
1047
1084
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1048
1085
 
1049
1086
 
 
1087
@deprecated_function(deprecated_in((2, 2, 0)))
1050
1088
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1051
1089
    """Filter view_revisions based on revision ranges.
1052
1090
 
1061
1099
 
1062
1100
    :return: The filtered view_revisions.
1063
1101
    """
1064
 
    # This method is no longer called by the main code path.
1065
 
    # It may be removed soon. IGC 20090127
1066
1102
    if start_rev_id or end_rev_id:
1067
1103
        revision_ids = [r for r, n, d in view_revisions]
1068
1104
        if start_rev_id:
1174
1210
    return result
1175
1211
 
1176
1212
 
 
1213
@deprecated_function(deprecated_in((2, 2, 0)))
1177
1214
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1178
1215
                       include_merges=True):
1179
1216
    """Produce an iterator of revisions to show
1180
1217
    :return: an iterator of (revision_id, revno, merge_depth)
1181
1218
    (if there is no revno for a revision, None is supplied)
1182
1219
    """
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
1220
    if not include_merges:
1187
1221
        revision_ids = mainline_revs[1:]
1188
1222
        if direction == 'reverse':
1283
1317
        one (2) should be used.
1284
1318
 
1285
1319
    - 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.
 
1320
        merge revisions.  If not, then only mainline revisions will be passed
 
1321
        to the formatter.
1289
1322
 
1290
1323
    - preferred_levels is the number of levels this formatter defaults to.
1291
1324
        The default value is zero meaning display all levels.
1292
1325
        This value is only relevant if supports_merge_revisions is True.
1293
1326
 
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
1327
    - supports_tags must be True if this log formatter supports tags.
1299
1328
        Otherwise the tags attribute may not be populated.
1300
1329
 
1311
1340
    preferred_levels = 0
1312
1341
 
1313
1342
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1314
 
                 delta_format=None, levels=None):
 
1343
                 delta_format=None, levels=None, show_advice=False,
 
1344
                 to_exact_file=None, author_list_handler=None):
1315
1345
        """Create a LogFormatter.
1316
1346
 
1317
1347
        :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.
1318
1350
        :param show_ids: if True, revision-ids are to be displayed
1319
1351
        :param show_timezone: the timezone to use
1320
1352
        :param delta_format: the level of delta information to display
1321
 
          or None to leave it u to the formatter to decide
 
1353
          or None to leave it to the formatter to decide
1322
1354
        :param levels: the number of levels to display; None or -1 to
1323
1355
          let the log formatter decide.
 
1356
        :param show_advice: whether to show advice at the end of the
 
1357
          log or not
 
1358
        :param author_list_handler: callable generating a list of
 
1359
          authors to display for a given revision
1324
1360
        """
1325
1361
        self.to_file = to_file
1326
1362
        # 'exact' stream used to show diff, it should print content 'as is'
1327
1363
        # 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)
 
1364
        if to_exact_file is not None:
 
1365
            self.to_exact_file = to_exact_file
 
1366
        else:
 
1367
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1368
            # for code that expects to get diffs to pass in the exact file
 
1369
            # stream
 
1370
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1329
1371
        self.show_ids = show_ids
1330
1372
        self.show_timezone = show_timezone
1331
1373
        if delta_format is None:
1333
1375
            delta_format = 2 # long format
1334
1376
        self.delta_format = delta_format
1335
1377
        self.levels = levels
 
1378
        self._show_advice = show_advice
 
1379
        self._merge_count = 0
 
1380
        self._author_list_handler = author_list_handler
1336
1381
 
1337
1382
    def get_levels(self):
1338
1383
        """Get the number of levels to display or 0 for all."""
1339
1384
        if getattr(self, 'supports_merge_revisions', False):
1340
1385
            if self.levels is None or self.levels == -1:
1341
 
                return self.preferred_levels
1342
 
            else:
1343
 
                return self.levels
1344
 
        return 1
 
1386
                self.levels = self.preferred_levels
 
1387
        else:
 
1388
            self.levels = 1
 
1389
        return self.levels
1345
1390
 
1346
1391
    def log_revision(self, revision):
1347
1392
        """Log a revision.
1350
1395
        """
1351
1396
        raise NotImplementedError('not implemented in abstract base')
1352
1397
 
 
1398
    def show_advice(self):
 
1399
        """Output user advice, if any, when the log is completed."""
 
1400
        if self._show_advice and self.levels == 1 and self._merge_count > 0:
 
1401
            advice_sep = self.get_advice_separator()
 
1402
            if advice_sep:
 
1403
                self.to_file.write(advice_sep)
 
1404
            self.to_file.write(
 
1405
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1406
 
 
1407
    def get_advice_separator(self):
 
1408
        """Get the text separating the log from the closing advice."""
 
1409
        return ''
 
1410
 
1353
1411
    def short_committer(self, rev):
1354
1412
        name, address = config.parse_username(rev.committer)
1355
1413
        if name:
1357
1415
        return address
1358
1416
 
1359
1417
    def short_author(self, rev):
1360
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1361
 
        if name:
1362
 
            return name
1363
 
        return address
 
1418
        return self.authors(rev, 'first', short=True, sep=', ')
 
1419
 
 
1420
    def authors(self, rev, who, short=False, sep=None):
 
1421
        """Generate list of authors, taking --authors option into account.
 
1422
 
 
1423
        The caller has to specify the name of a author list handler,
 
1424
        as provided by the author list registry, using the ``who``
 
1425
        argument.  That name only sets a default, though: when the
 
1426
        user selected a different author list generation using the
 
1427
        ``--authors`` command line switch, as represented by the
 
1428
        ``author_list_handler`` constructor argument, that value takes
 
1429
        precedence.
 
1430
 
 
1431
        :param rev: The revision for which to generate the list of authors.
 
1432
        :param who: Name of the default handler.
 
1433
        :param short: Whether to shorten names to either name or address.
 
1434
        :param sep: What separator to use for automatic concatenation.
 
1435
        """
 
1436
        if self._author_list_handler is not None:
 
1437
            # The user did specify --authors, which overrides the default
 
1438
            author_list_handler = self._author_list_handler
 
1439
        else:
 
1440
            # The user didn't specify --authors, so we use the caller's default
 
1441
            author_list_handler = author_list_registry.get(who)
 
1442
        names = author_list_handler(rev)
 
1443
        if short:
 
1444
            for i in range(len(names)):
 
1445
                name, address = config.parse_username(names[i])
 
1446
                if name:
 
1447
                    names[i] = name
 
1448
                else:
 
1449
                    names[i] = address
 
1450
        if sep is not None:
 
1451
            names = sep.join(names)
 
1452
        return names
 
1453
 
 
1454
    def merge_marker(self, revision):
 
1455
        """Get the merge marker to include in the output or '' if none."""
 
1456
        if len(revision.rev.parent_ids) > 1:
 
1457
            self._merge_count += 1
 
1458
            return ' [merge]'
 
1459
        else:
 
1460
            return ''
1364
1461
 
1365
1462
    def show_properties(self, revision, indent):
1366
1463
        """Displays the custom properties returned by each registered handler.
1367
1464
 
1368
1465
        If a registered handler raises an error it is propagated.
1369
1466
        """
 
1467
        for line in self.custom_properties(revision):
 
1468
            self.to_file.write("%s%s\n" % (indent, line))
 
1469
 
 
1470
    def custom_properties(self, revision):
 
1471
        """Format the custom properties returned by each registered handler.
 
1472
 
 
1473
        If a registered handler raises an error it is propagated.
 
1474
 
 
1475
        :return: a list of formatted lines (excluding trailing newlines)
 
1476
        """
 
1477
        lines = self._foreign_info_properties(revision)
1370
1478
        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')
 
1479
            lines.extend(self._format_properties(handler(revision)))
 
1480
        return lines
 
1481
 
 
1482
    def _foreign_info_properties(self, rev):
 
1483
        """Custom log displayer for foreign revision identifiers.
 
1484
 
 
1485
        :param rev: Revision object.
 
1486
        """
 
1487
        # Revision comes directly from a foreign repository
 
1488
        if isinstance(rev, foreign.ForeignRevision):
 
1489
            return self._format_properties(
 
1490
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1491
 
 
1492
        # Imported foreign revision revision ids always contain :
 
1493
        if not ":" in rev.revision_id:
 
1494
            return []
 
1495
 
 
1496
        # Revision was once imported from a foreign repository
 
1497
        try:
 
1498
            foreign_revid, mapping = \
 
1499
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
 
1500
        except errors.InvalidRevisionId:
 
1501
            return []
 
1502
 
 
1503
        return self._format_properties(
 
1504
            mapping.vcs.show_foreign_revid(foreign_revid))
 
1505
 
 
1506
    def _format_properties(self, properties):
 
1507
        lines = []
 
1508
        for key, value in properties.items():
 
1509
            lines.append(key + ': ' + value)
 
1510
        return lines
1373
1511
 
1374
1512
    def show_diff(self, to_file, diff, indent):
1375
1513
        for l in diff.rstrip().split('\n'):
1376
1514
            to_file.write(indent + '%s\n' % (l,))
1377
1515
 
1378
1516
 
 
1517
# Separator between revisions in long format
 
1518
_LONG_SEP = '-' * 60
 
1519
 
 
1520
 
1379
1521
class LongLogFormatter(LogFormatter):
1380
1522
 
1381
1523
    supports_merge_revisions = True
 
1524
    preferred_levels = 1
1382
1525
    supports_delta = True
1383
1526
    supports_tags = True
1384
1527
    supports_diff = True
1385
1528
 
 
1529
    def __init__(self, *args, **kwargs):
 
1530
        super(LongLogFormatter, self).__init__(*args, **kwargs)
 
1531
        if self.show_timezone == 'original':
 
1532
            self.date_string = self._date_string_original_timezone
 
1533
        else:
 
1534
            self.date_string = self._date_string_with_timezone
 
1535
 
 
1536
    def _date_string_with_timezone(self, rev):
 
1537
        return format_date(rev.timestamp, rev.timezone or 0,
 
1538
                           self.show_timezone)
 
1539
 
 
1540
    def _date_string_original_timezone(self, rev):
 
1541
        return format_date_with_offset_in_original_timezone(rev.timestamp,
 
1542
            rev.timezone or 0)
 
1543
 
1386
1544
    def log_revision(self, revision):
1387
1545
        """Log a revision, either merged or not."""
1388
1546
        indent = '    ' * revision.merge_depth
1389
 
        to_file = self.to_file
1390
 
        to_file.write(indent + '-' * 60 + '\n')
 
1547
        lines = [_LONG_SEP]
1391
1548
        if revision.revno is not None:
1392
 
            to_file.write(indent + 'revno: %s\n' % (revision.revno,))
 
1549
            lines.append('revno: %s%s' % (revision.revno,
 
1550
                self.merge_marker(revision)))
1393
1551
        if revision.tags:
1394
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
 
1552
            lines.append('tags: %s' % (', '.join(revision.tags)))
1395
1553
        if self.show_ids:
1396
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1397
 
            to_file.write('\n')
 
1554
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1398
1555
            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)
 
1556
                lines.append('parent: %s' % (parent_id,))
 
1557
        lines.extend(self.custom_properties(revision.rev))
1401
1558
 
1402
1559
        committer = revision.rev.committer
1403
 
        authors = revision.rev.get_apparent_authors()
 
1560
        authors = self.authors(revision.rev, 'all')
1404
1561
        if authors != [committer]:
1405
 
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1406
 
        to_file.write(indent + 'committer: %s\n' % (committer,))
 
1562
            lines.append('author: %s' % (", ".join(authors),))
 
1563
        lines.append('committer: %s' % (committer,))
1407
1564
 
1408
1565
        branch_nick = revision.rev.properties.get('branch-nick', None)
1409
1566
        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')
 
1567
            lines.append('branch nick: %s' % (branch_nick,))
 
1568
 
 
1569
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1570
 
 
1571
        lines.append('message:')
1418
1572
        if not revision.rev.message:
1419
 
            to_file.write(indent + '  (no message)\n')
 
1573
            lines.append('  (no message)')
1420
1574
        else:
1421
1575
            message = revision.rev.message.rstrip('\r\n')
1422
1576
            for l in message.split('\n'):
1423
 
                to_file.write(indent + '  %s\n' % (l,))
 
1577
                lines.append('  %s' % (l,))
 
1578
 
 
1579
        # Dump the output, appending the delta and diff if requested
 
1580
        to_file = self.to_file
 
1581
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1424
1582
        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)
 
1583
            # Use the standard status output to display changes
 
1584
            from bzrlib.delta import report_delta
 
1585
            report_delta(to_file, revision.delta, short_status=False, 
 
1586
                         show_ids=self.show_ids, indent=indent)
1428
1587
        if revision.diff is not None:
1429
1588
            to_file.write(indent + 'diff:\n')
 
1589
            to_file.flush()
1430
1590
            # Note: we explicitly don't indent the diff (relative to the
1431
1591
            # revision information) so that the output can be fed to patch -p0
1432
1592
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1593
            self.to_exact_file.flush()
 
1594
 
 
1595
    def get_advice_separator(self):
 
1596
        """Get the text separating the log from the closing advice."""
 
1597
        return '-' * 60 + '\n'
1433
1598
 
1434
1599
 
1435
1600
class ShortLogFormatter(LogFormatter):
1465
1630
        offset = ' ' * (revno_width + 1)
1466
1631
 
1467
1632
        to_file = self.to_file
1468
 
        is_merge = ''
1469
 
        if len(revision.rev.parent_ids) > 1:
1470
 
            is_merge = ' [merge]'
1471
1633
        tags = ''
1472
1634
        if revision.tags:
1473
1635
            tags = ' {%s}' % (', '.join(revision.tags))
1477
1639
                            revision.rev.timezone or 0,
1478
1640
                            self.show_timezone, date_fmt="%Y-%m-%d",
1479
1641
                            show_offset=False),
1480
 
                tags, is_merge))
 
1642
                tags, self.merge_marker(revision)))
1481
1643
        self.show_properties(revision.rev, indent+offset)
1482
1644
        if self.show_ids:
1483
1645
            to_file.write(indent + offset + 'revision-id:%s\n'
1490
1652
                to_file.write(indent + offset + '%s\n' % (l,))
1491
1653
 
1492
1654
        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)
 
1655
            # Use the standard status output to display changes
 
1656
            from bzrlib.delta import report_delta
 
1657
            report_delta(to_file, revision.delta, 
 
1658
                         short_status=self.delta_format==1, 
 
1659
                         show_ids=self.show_ids, indent=indent + offset)
1495
1660
        if revision.diff is not None:
1496
1661
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1497
1662
        to_file.write('\n')
1505
1670
 
1506
1671
    def __init__(self, *args, **kwargs):
1507
1672
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1508
 
        self._max_chars = terminal_width() - 1
 
1673
        width = terminal_width()
 
1674
        if width is not None:
 
1675
            # we need one extra space for terminals that wrap on last char
 
1676
            width = width - 1
 
1677
        self._max_chars = width
1509
1678
 
1510
1679
    def truncate(self, str, max_len):
1511
 
        if len(str) <= max_len:
 
1680
        if max_len is None or len(str) <= max_len:
1512
1681
            return str
1513
 
        return str[:max_len-3]+'...'
 
1682
        return str[:max_len-3] + '...'
1514
1683
 
1515
1684
    def date_string(self, rev):
1516
1685
        return format_date(rev.timestamp, rev.timezone or 0,
1568
1737
                               self.show_timezone,
1569
1738
                               date_fmt='%Y-%m-%d',
1570
1739
                               show_offset=False)
1571
 
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1740
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1741
        committer_str = committer_str.replace(' <', '  <')
1572
1742
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1573
1743
 
1574
1744
        if revision.delta is not None and revision.delta.has_changed():
1639
1809
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1640
1810
 
1641
1811
 
 
1812
def author_list_all(rev):
 
1813
    return rev.get_apparent_authors()[:]
 
1814
 
 
1815
 
 
1816
def author_list_first(rev):
 
1817
    lst = rev.get_apparent_authors()
 
1818
    try:
 
1819
        return [lst[0]]
 
1820
    except IndexError:
 
1821
        return []
 
1822
 
 
1823
 
 
1824
def author_list_committer(rev):
 
1825
    return [rev.committer]
 
1826
 
 
1827
 
 
1828
author_list_registry = registry.Registry()
 
1829
 
 
1830
author_list_registry.register('all', author_list_all,
 
1831
                              'All authors')
 
1832
 
 
1833
author_list_registry.register('first', author_list_first,
 
1834
                              'The first author')
 
1835
 
 
1836
author_list_registry.register('committer', author_list_committer,
 
1837
                              'The committer')
 
1838
 
 
1839
 
1642
1840
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1643
1841
    # deprecated; for compatibility
1644
1842
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1795
1993
        lf.log_revision(lr)
1796
1994
 
1797
1995
 
1798
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
1996
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1799
1997
    """Find file-ids and kinds given a list of files and a revision range.
1800
1998
 
1801
1999
    We search for files at the end of the range. If not found there,
1805
2003
    :param file_list: the list of paths given on the command line;
1806
2004
      the first of these can be a branch location or a file path,
1807
2005
      the remainder must be file paths
 
2006
    :param add_cleanup: When the branch returned is read locked,
 
2007
      an unlock call will be queued to the cleanup.
1808
2008
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1809
2009
      info_list is a list of (relative_path, file_id, kind) tuples where
1810
2010
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
2011
      branch will be read-locked.
1811
2012
    """
1812
2013
    from builtins import _get_revision_range, safe_relpath_files
1813
2014
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
2015
    add_cleanup(b.lock_read().unlock)
1814
2016
    # XXX: It's damn messy converting a list of paths to relative paths when
1815
2017
    # those paths might be deleted ones, they might be on a case-insensitive
1816
2018
    # filesystem and/or they might be in silly locations (like another branch).
1826
2028
    info_list = []
1827
2029
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
1828
2030
        "log")
 
2031
    if relpaths in ([], [u'']):
 
2032
        return b, [], start_rev_info, end_rev_info
1829
2033
    if start_rev_info is None and end_rev_info is None:
1830
2034
        if tree is None:
1831
2035
            tree = b.basis_tree()
1892
2096
 
1893
2097
 
1894
2098
properties_handler_registry = registry.Registry()
1895
 
properties_handler_registry.register_lazy("foreign",
1896
 
                                          "bzrlib.foreign",
1897
 
                                          "show_foreign_properties")
 
2099
 
 
2100
# Use the properties handlers to print out bug information if available
 
2101
def _bugs_properties_handler(revision):
 
2102
    if revision.properties.has_key('bugs'):
 
2103
        bug_lines = revision.properties['bugs'].split('\n')
 
2104
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2105
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2106
                          len(row) > 1 and row[1] == 'fixed']
 
2107
 
 
2108
        if fixed_bug_urls:
 
2109
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2110
    return {}
 
2111
 
 
2112
properties_handler_registry.register('bugs_properties_handler',
 
2113
                                     _bugs_properties_handler)
1898
2114
 
1899
2115
 
1900
2116
# adapters which revision ids to log are filtered. When log is called, the