/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jelmer Vernooij
  • Date: 2009-04-03 00:02:29 UTC
  • mto: This revision was merged to the branch mainline in revision 4250.
  • Revision ID: jelmer@samba.org-20090403000229-hfj4xd3od30j9udf
Rename BzrDir.push() to BzrDir.push_branch().

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2009 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
69
69
    config,
70
70
    diff,
71
71
    errors,
72
 
    foreign,
73
72
    repository as _mod_repository,
74
73
    revision as _mod_revision,
75
74
    revisionspec,
83
82
    )
84
83
from bzrlib.osutils import (
85
84
    format_date,
86
 
    format_date_with_offset_in_original_timezone,
87
85
    get_terminal_encoding,
88
86
    re_compile_checked,
89
87
    terminal_width,
90
88
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
94
 
    )
95
89
 
96
90
 
97
91
def find_touching_revisions(branch, file_id):
109
103
    last_path = None
110
104
    revno = 1
111
105
    for revision_id in branch.revision_history():
112
 
        this_inv = branch.repository.get_inventory(revision_id)
 
106
        this_inv = branch.repository.get_revision_inventory(revision_id)
113
107
        if file_id in this_inv:
114
108
            this_ie = this_inv[file_id]
115
109
            this_path = this_inv.id2path(file_id)
220
214
    'direction': 'reverse',
221
215
    'levels': 1,
222
216
    'generate_tags': True,
223
 
    'exclude_common_ancestry': False,
224
217
    '_match_using_deltas': True,
225
218
    }
226
219
 
227
220
 
228
221
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
 
                          start_revision=None, end_revision=None, limit=None,
230
 
                          message_search=None, levels=1, generate_tags=True,
231
 
                          delta_type=None,
232
 
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
234
 
                          ):
 
222
    start_revision=None, end_revision=None, limit=None,
 
223
    message_search=None, levels=1, generate_tags=True, delta_type=None,
 
224
    diff_type=None, _match_using_deltas=True):
235
225
    """Convenience function for making a logging request dictionary.
236
226
 
237
227
    Using this function may make code slightly safer by ensuring
275
265
      algorithm used for matching specific_fileids. This parameter
276
266
      may be removed in the future so bzrlib client code should NOT
277
267
      use it.
278
 
 
279
 
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
 
      range operator or as a graph difference.
281
268
    """
282
269
    return {
283
270
        'direction': direction,
290
277
        'generate_tags': generate_tags,
291
278
        'delta_type': delta_type,
292
279
        'diff_type': diff_type,
293
 
        'exclude_common_ancestry': exclude_common_ancestry,
294
280
        # Add 'private' attributes for features that may be deprecated
295
281
        '_match_using_deltas': _match_using_deltas,
 
282
        '_allow_single_merge_revision': True,
296
283
    }
297
284
 
298
285
 
316
303
 
317
304
 
318
305
class Logger(object):
319
 
    """An object that generates, formats and displays a log."""
 
306
    """An object the generates, formats and displays a log."""
320
307
 
321
308
    def __init__(self, branch, rqst):
322
309
        """Create a Logger.
361
348
            rqst['delta_type'] = None
362
349
        if not getattr(lf, 'supports_diff', False):
363
350
            rqst['diff_type'] = None
 
351
        if not getattr(lf, 'supports_merge_revisions', False):
 
352
            rqst['_allow_single_merge_revision'] = getattr(lf,
 
353
                'supports_single_merge_revision', False)
364
354
 
365
355
        # Find and print the interesting revisions
366
356
        generator = self._generator_factory(self.branch, rqst)
397
387
        :return: An iterator yielding LogRevision objects.
398
388
        """
399
389
        rqst = self.rqst
400
 
        levels = rqst.get('levels')
401
 
        limit = rqst.get('limit')
402
 
        diff_type = rqst.get('diff_type')
403
390
        log_count = 0
404
391
        revision_iterator = self._create_log_revision_iterator()
405
392
        for revs in revision_iterator:
406
393
            for (rev_id, revno, merge_depth), rev, delta in revs:
407
394
                # 0 levels means show everything; merge_depth counts from 0
 
395
                levels = rqst.get('levels')
408
396
                if levels != 0 and merge_depth >= levels:
409
397
                    continue
410
 
                if diff_type is None:
411
 
                    diff = None
412
 
                else:
413
 
                    diff = self._format_diff(rev, rev_id, diff_type)
 
398
                diff = self._format_diff(rev, rev_id)
414
399
                yield LogRevision(rev, revno, merge_depth, delta,
415
400
                    self.rev_tag_dict.get(rev_id), diff)
 
401
                limit = rqst.get('limit')
416
402
                if limit:
417
403
                    log_count += 1
418
404
                    if log_count >= limit:
419
405
                        return
420
406
 
421
 
    def _format_diff(self, rev, rev_id, diff_type):
 
407
    def _format_diff(self, rev, rev_id):
 
408
        diff_type = self.rqst.get('diff_type')
 
409
        if diff_type is None:
 
410
            return None
422
411
        repo = self.branch.repository
423
412
        if len(rev.parent_ids) == 0:
424
413
            ancestor_id = _mod_revision.NULL_REVISION
463
452
        generate_merge_revisions = rqst.get('levels') != 1
464
453
        delayed_graph_generation = not rqst.get('specific_fileids') and (
465
454
                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'))
 
455
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
456
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
 
457
            rqst.get('_allow_single_merge_revision'),
 
458
            delayed_graph_generation=delayed_graph_generation)
472
459
 
473
460
        # Apply the other filters
474
461
        return make_log_rev_iterator(self.branch, view_revisions,
481
468
        # Note that we always generate the merge revisions because
482
469
        # filter_revisions_touching_file_id() requires them ...
483
470
        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'))
 
471
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
472
            self.end_rev_id, rqst.get('direction'), True,
 
473
            rqst.get('_allow_single_merge_revision'))
488
474
        if not isinstance(view_revisions, list):
489
475
            view_revisions = list(view_revisions)
490
476
        view_revisions = _filter_revisions_touching_file_id(self.branch,
495
481
 
496
482
 
497
483
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
 
                         ):
 
484
    generate_merge_revisions, allow_single_merge_revision,
 
485
    delayed_graph_generation=False):
502
486
    """Calculate the revisions to view.
503
487
 
504
488
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
505
489
             a list of the same tuples.
506
490
    """
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
491
    br_revno, br_rev_id = branch.last_revision_info()
513
492
    if br_revno == 0:
514
493
        return []
515
494
 
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)
 
495
    # If a single revision is requested, check we can handle it
 
496
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
 
497
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
 
498
    if generate_single_revision:
 
499
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno,
 
500
            allow_single_merge_revision)
 
501
 
 
502
    # If we only want to see linear revisions, we can iterate ...
 
503
    if not generate_merge_revisions:
 
504
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
505
            direction)
528
506
    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
535
 
 
536
 
 
537
 
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
 
507
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
508
            direction, delayed_graph_generation)
 
509
 
 
510
 
 
511
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno,
 
512
    allow_single_merge_revision):
538
513
    if rev_id == br_rev_id:
539
514
        # It's the tip
540
515
        return [(br_rev_id, br_revno, 0)]
541
516
    else:
542
517
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
518
        if len(revno) > 1 and not allow_single_merge_revision:
 
519
            # It's a merge revision and the log formatter is
 
520
            # completely brain dead. This "feature" of allowing
 
521
            # log formatters incapable of displaying dotted revnos
 
522
            # ought to be deprecated IMNSHO. IGC 20091022
 
523
            raise errors.BzrCommandError('Selected log formatter only'
 
524
                ' supports mainline revisions.')
543
525
        revno_str = '.'.join(str(n) for n in revno)
544
526
        return [(rev_id, revno_str, 0)]
545
527
 
555
537
        except _StartNotLinearAncestor:
556
538
            raise errors.BzrCommandError('Start revision not found in'
557
539
                ' left-hand history of end revision.')
 
540
    if direction == 'forward':
 
541
        result = reversed(result)
558
542
    return result
559
543
 
560
544
 
561
545
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
 
                            delayed_graph_generation,
563
 
                            exclude_common_ancestry=False):
 
546
    delayed_graph_generation):
564
547
    # On large trees, generating the merge graph can take 30-60 seconds
565
548
    # so we delay doing it until a merge is detected, incrementally
566
549
    # 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
550
    initial_revisions = []
572
551
    if delayed_graph_generation:
573
552
        try:
574
 
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
553
            for rev_id, revno, depth in \
 
554
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
576
555
                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
556
                    end_rev_id = rev_id
593
557
                    break
594
558
                else:
595
559
                    initial_revisions.append((rev_id, revno, depth))
596
560
            else:
597
561
                # No merged revisions found
598
 
                return initial_revisions
 
562
                if direction == 'reverse':
 
563
                    return initial_revisions
 
564
                elif direction == 'forward':
 
565
                    return reversed(initial_revisions)
 
566
                else:
 
567
                    raise ValueError('invalid direction %r' % direction)
599
568
        except _StartNotLinearAncestor:
600
569
            # A merge was never detected so the lower revision limit can't
601
570
            # be nested down somewhere
602
571
            raise errors.BzrCommandError('Start revision not found in'
603
572
                ' history of end revision.')
604
573
 
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
574
    # A log including nested merges is required. If the direction is reverse,
609
575
    # we rebase the initial merge depths so that the development line is
610
576
    # shown naturally, i.e. just like it is for linear logging. We can easily
612
578
    # indented at the end seems slightly nicer in that case.
613
579
    view_revisions = chain(iter(initial_revisions),
614
580
        _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
 
581
        rebase_initial_depths=direction == 'reverse'))
 
582
    if direction == 'reverse':
 
583
        return view_revisions
 
584
    elif direction == 'forward':
 
585
        # Forward means oldest first, adjusting for depth.
 
586
        view_revisions = reverse_by_depth(list(view_revisions))
 
587
        return _rebase_merge_depth(view_revisions)
 
588
    else:
 
589
        raise ValueError('invalid direction %r' % direction)
618
590
 
619
591
 
620
592
def _has_merges(branch, rev_id):
638
610
        else:
639
611
            # not obvious
640
612
            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
613
    return True
644
614
 
645
615
 
678
648
 
679
649
 
680
650
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
681
 
                          rebase_initial_depths=True,
682
 
                          exclude_common_ancestry=False):
 
651
    rebase_initial_depths=True):
683
652
    """Calculate revisions to view including merges, newest to oldest.
684
653
 
685
654
    :param branch: the branch
689
658
      revision is found?
690
659
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
691
660
    """
692
 
    if exclude_common_ancestry:
693
 
        stop_rule = 'with-merges-without-common-ancestry'
694
 
    else:
695
 
        stop_rule = 'with-merges'
696
661
    view_revisions = branch.iter_merge_sorted_revisions(
697
662
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
698
 
        stop_rule=stop_rule)
 
663
        stop_rule="with-merges")
699
664
    if not rebase_initial_depths:
700
665
        for (rev_id, merge_depth, revno, end_of_merge
701
666
             ) in view_revisions:
712
677
                depth_adjustment = merge_depth
713
678
            if depth_adjustment:
714
679
                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
680
                    depth_adjustment = merge_depth
720
681
                merge_depth -= depth_adjustment
721
682
            yield rev_id, '.'.join(map(str, revno)), merge_depth
722
683
 
723
684
 
724
 
@deprecated_function(deprecated_in((2, 2, 0)))
725
685
def calculate_view_revisions(branch, start_revision, end_revision, direction,
726
 
        specific_fileid, generate_merge_revisions):
 
686
        specific_fileid, generate_merge_revisions, allow_single_merge_revision):
727
687
    """Calculate the revisions to view.
728
688
 
729
689
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
730
690
             a list of the same tuples.
731
691
    """
 
692
    # This method is no longer called by the main code path.
 
693
    # It is retained for API compatibility and may be deprecated
 
694
    # soon. IGC 20090116
732
695
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
733
696
        end_revision)
734
697
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
735
 
        direction, generate_merge_revisions or specific_fileid))
 
698
        direction, generate_merge_revisions or specific_fileid,
 
699
        allow_single_merge_revision))
736
700
    if specific_fileid:
737
701
        view_revisions = _filter_revisions_touching_file_id(branch,
738
702
            specific_fileid, view_revisions,
1084
1048
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1085
1049
 
1086
1050
 
1087
 
@deprecated_function(deprecated_in((2, 2, 0)))
1088
1051
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1089
1052
    """Filter view_revisions based on revision ranges.
1090
1053
 
1099
1062
 
1100
1063
    :return: The filtered view_revisions.
1101
1064
    """
 
1065
    # This method is no longer called by the main code path.
 
1066
    # It may be removed soon. IGC 20090127
1102
1067
    if start_rev_id or end_rev_id:
1103
1068
        revision_ids = [r for r, n, d in view_revisions]
1104
1069
        if start_rev_id:
1210
1175
    return result
1211
1176
 
1212
1177
 
1213
 
@deprecated_function(deprecated_in((2, 2, 0)))
1214
1178
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1215
1179
                       include_merges=True):
1216
1180
    """Produce an iterator of revisions to show
1217
1181
    :return: an iterator of (revision_id, revno, merge_depth)
1218
1182
    (if there is no revno for a revision, None is supplied)
1219
1183
    """
 
1184
    # This method is no longer called by the main code path.
 
1185
    # It is retained for API compatibility and may be deprecated
 
1186
    # soon. IGC 20090127
1220
1187
    if not include_merges:
1221
1188
        revision_ids = mainline_revs[1:]
1222
1189
        if direction == 'reverse':
1317
1284
        one (2) should be used.
1318
1285
 
1319
1286
    - supports_merge_revisions must be True if this log formatter supports
1320
 
        merge revisions.  If not, then only mainline revisions will be passed
1321
 
        to the formatter.
 
1287
        merge revisions.  If not, and if supports_single_merge_revision is
 
1288
        also not True, then only mainline revisions will be passed to the
 
1289
        formatter.
1322
1290
 
1323
1291
    - preferred_levels is the number of levels this formatter defaults to.
1324
1292
        The default value is zero meaning display all levels.
1325
1293
        This value is only relevant if supports_merge_revisions is True.
1326
1294
 
 
1295
    - supports_single_merge_revision must be True if this log formatter
 
1296
        supports logging only a single merge revision.  This flag is
 
1297
        only relevant if supports_merge_revisions is not True.
 
1298
 
1327
1299
    - supports_tags must be True if this log formatter supports tags.
1328
1300
        Otherwise the tags attribute may not be populated.
1329
1301
 
1340
1312
    preferred_levels = 0
1341
1313
 
1342
1314
    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):
 
1315
                 delta_format=None, levels=None):
1345
1316
        """Create a LogFormatter.
1346
1317
 
1347
1318
        :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
1319
        :param show_ids: if True, revision-ids are to be displayed
1351
1320
        :param show_timezone: the timezone to use
1352
1321
        :param delta_format: the level of delta information to display
1353
 
          or None to leave it to the formatter to decide
 
1322
          or None to leave it u to the formatter to decide
1354
1323
        :param levels: the number of levels to display; None or -1 to
1355
1324
          let the log formatter decide.
1356
 
        :param show_advice: whether to show advice at the end of the
1357
 
          log or not
1358
1325
        """
1359
1326
        self.to_file = to_file
1360
1327
        # 'exact' stream used to show diff, it should print content 'as is'
1361
1328
        # 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)
 
1329
        self.to_exact_file = getattr(to_file, 'stream', to_file)
1369
1330
        self.show_ids = show_ids
1370
1331
        self.show_timezone = show_timezone
1371
1332
        if delta_format is None:
1373
1334
            delta_format = 2 # long format
1374
1335
        self.delta_format = delta_format
1375
1336
        self.levels = levels
1376
 
        self._show_advice = show_advice
1377
1337
        self._merge_count = 0
1378
1338
 
1379
1339
    def get_levels(self):
1380
1340
        """Get the number of levels to display or 0 for all."""
1381
1341
        if getattr(self, 'supports_merge_revisions', False):
1382
1342
            if self.levels is None or self.levels == -1:
1383
 
                self.levels = self.preferred_levels
1384
 
        else:
1385
 
            self.levels = 1
1386
 
        return self.levels
 
1343
                return self.preferred_levels
 
1344
            else:
 
1345
                return self.levels
 
1346
        return 1
1387
1347
 
1388
1348
    def log_revision(self, revision):
1389
1349
        """Log a revision.
1394
1354
 
1395
1355
    def show_advice(self):
1396
1356
        """Output user advice, if any, when the log is completed."""
1397
 
        if self._show_advice and self.levels == 1 and self._merge_count > 0:
 
1357
        if self.levels == 1 and self._merge_count > 0:
1398
1358
            advice_sep = self.get_advice_separator()
1399
1359
            if advice_sep:
1400
1360
                self.to_file.write(advice_sep)
1401
1361
            self.to_file.write(
1402
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1362
                "Use --levels 0 (or -n0) to see merged revisions.\n")
1403
1363
 
1404
1364
    def get_advice_separator(self):
1405
1365
        """Get the text separating the log from the closing advice."""
1430
1390
 
1431
1391
        If a registered handler raises an error it is propagated.
1432
1392
        """
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
1393
        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):
1449
 
        """Custom log displayer for foreign revision identifiers.
1450
 
 
1451
 
        :param rev: Revision object.
1452
 
        """
1453
 
        # Revision comes directly from a foreign repository
1454
 
        if isinstance(rev, foreign.ForeignRevision):
1455
 
            return self._format_properties(
1456
 
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1457
 
 
1458
 
        # Imported foreign revision revision ids always contain :
1459
 
        if not ":" in rev.revision_id:
1460
 
            return []
1461
 
 
1462
 
        # Revision was once imported from a foreign repository
1463
 
        try:
1464
 
            foreign_revid, mapping = \
1465
 
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1466
 
        except errors.InvalidRevisionId:
1467
 
            return []
1468
 
 
1469
 
        return self._format_properties(
1470
 
            mapping.vcs.show_foreign_revid(foreign_revid))
1471
 
 
1472
 
    def _format_properties(self, properties):
1473
 
        lines = []
1474
 
        for key, value in properties.items():
1475
 
            lines.append(key + ': ' + value)
1476
 
        return lines
 
1394
            for key, value in handler(revision).items():
 
1395
                self.to_file.write(indent + key + ': ' + value + '\n')
1477
1396
 
1478
1397
    def show_diff(self, to_file, diff, indent):
1479
1398
        for l in diff.rstrip().split('\n'):
1480
1399
            to_file.write(indent + '%s\n' % (l,))
1481
1400
 
1482
1401
 
1483
 
# Separator between revisions in long format
1484
 
_LONG_SEP = '-' * 60
1485
 
 
1486
 
 
1487
1402
class LongLogFormatter(LogFormatter):
1488
1403
 
1489
1404
    supports_merge_revisions = True
1492
1407
    supports_tags = True
1493
1408
    supports_diff = True
1494
1409
 
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
1410
    def log_revision(self, revision):
1511
1411
        """Log a revision, either merged or not."""
1512
1412
        indent = '    ' * revision.merge_depth
1513
 
        lines = [_LONG_SEP]
 
1413
        to_file = self.to_file
 
1414
        to_file.write(indent + '-' * 60 + '\n')
1514
1415
        if revision.revno is not None:
1515
 
            lines.append('revno: %s%s' % (revision.revno,
 
1416
            to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
1516
1417
                self.merge_marker(revision)))
1517
1418
        if revision.tags:
1518
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1419
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1519
1420
        if self.show_ids:
1520
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1421
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
 
1422
            to_file.write('\n')
1521
1423
            for parent_id in revision.rev.parent_ids:
1522
 
                lines.append('parent: %s' % (parent_id,))
1523
 
        lines.extend(self.custom_properties(revision.rev))
 
1424
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
 
1425
        self.show_properties(revision.rev, indent)
1524
1426
 
1525
1427
        committer = revision.rev.committer
1526
1428
        authors = revision.rev.get_apparent_authors()
1527
1429
        if authors != [committer]:
1528
 
            lines.append('author: %s' % (", ".join(authors),))
1529
 
        lines.append('committer: %s' % (committer,))
 
1430
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
 
1431
        to_file.write(indent + 'committer: %s\n' % (committer,))
1530
1432
 
1531
1433
        branch_nick = revision.rev.properties.get('branch-nick', None)
1532
1434
        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:')
 
1435
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
 
1436
 
 
1437
        date_str = format_date(revision.rev.timestamp,
 
1438
                               revision.rev.timezone or 0,
 
1439
                               self.show_timezone)
 
1440
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
 
1441
 
 
1442
        to_file.write(indent + 'message:\n')
1538
1443
        if not revision.rev.message:
1539
 
            lines.append('  (no message)')
 
1444
            to_file.write(indent + '  (no message)\n')
1540
1445
        else:
1541
1446
            message = revision.rev.message.rstrip('\r\n')
1542
1447
            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)))
 
1448
                to_file.write(indent + '  %s\n' % (l,))
1548
1449
        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)
 
1450
            # We don't respect delta_format for compatibility
 
1451
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1452
                                short_status=False)
1553
1453
        if revision.diff is not None:
1554
1454
            to_file.write(indent + 'diff:\n')
1555
 
            to_file.flush()
1556
1455
            # Note: we explicitly don't indent the diff (relative to the
1557
1456
            # revision information) so that the output can be fed to patch -p0
1558
1457
            self.show_diff(self.to_exact_file, revision.diff, indent)
1559
 
            self.to_exact_file.flush()
1560
1458
 
1561
1459
    def get_advice_separator(self):
1562
1460
        """Get the text separating the log from the closing advice."""
1618
1516
                to_file.write(indent + offset + '%s\n' % (l,))
1619
1517
 
1620
1518
        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)
 
1519
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1520
                                short_status=self.delta_format==1)
1626
1521
        if revision.diff is not None:
1627
1522
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1628
1523
        to_file.write('\n')
1636
1531
 
1637
1532
    def __init__(self, *args, **kwargs):
1638
1533
        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
 
1534
        self._max_chars = terminal_width() - 1
1644
1535
 
1645
1536
    def truncate(self, str, max_len):
1646
 
        if max_len is None or len(str) <= max_len:
 
1537
        if len(str) <= max_len:
1647
1538
            return str
1648
 
        return str[:max_len-3] + '...'
 
1539
        return str[:max_len-3]+'...'
1649
1540
 
1650
1541
    def date_string(self, rev):
1651
1542
        return format_date(rev.timestamp, rev.timezone or 0,
1703
1594
                               self.show_timezone,
1704
1595
                               date_fmt='%Y-%m-%d',
1705
1596
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
 
1597
        committer_str = revision.rev.committer.replace (' <', '  <')
1707
1598
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1708
1599
 
1709
1600
        if revision.delta is not None and revision.delta.has_changed():
1943
1834
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
1835
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
1836
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
 
      branch will be read-locked.
1947
1837
    """
1948
1838
    from builtins import _get_revision_range, safe_relpath_files
1949
1839
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
1951
1840
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
1841
    # those paths might be deleted ones, they might be on a case-insensitive
1953
1842
    # filesystem and/or they might be in silly locations (like another branch).
1963
1852
    info_list = []
1964
1853
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
1965
1854
        "log")
1966
 
    if relpaths in ([], [u'']):
1967
 
        return b, [], start_rev_info, end_rev_info
1968
1855
    if start_rev_info is None and end_rev_info is None:
1969
1856
        if tree is None:
1970
1857
            tree = b.basis_tree()
2031
1918
 
2032
1919
 
2033
1920
properties_handler_registry = registry.Registry()
2034
 
 
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)
 
1921
properties_handler_registry.register_lazy("foreign",
 
1922
                                          "bzrlib.foreign",
 
1923
                                          "show_foreign_properties")
2049
1924
 
2050
1925
 
2051
1926
# adapters which revision ids to log are filtered. When log is called, the