/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

Merge jam python groupcompress implementation

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)
367
357
        for lr in generator.iter_log_revisions():
368
358
            lf.log_revision(lr)
369
 
        lf.show_advice()
370
359
 
371
360
    def _generator_factory(self, branch, rqst):
372
361
        """Make the LogGenerator object to use.
397
386
        :return: An iterator yielding LogRevision objects.
398
387
        """
399
388
        rqst = self.rqst
400
 
        levels = rqst.get('levels')
401
 
        limit = rqst.get('limit')
402
 
        diff_type = rqst.get('diff_type')
403
389
        log_count = 0
404
390
        revision_iterator = self._create_log_revision_iterator()
405
391
        for revs in revision_iterator:
406
392
            for (rev_id, revno, merge_depth), rev, delta in revs:
407
393
                # 0 levels means show everything; merge_depth counts from 0
 
394
                levels = rqst.get('levels')
408
395
                if levels != 0 and merge_depth >= levels:
409
396
                    continue
410
 
                if diff_type is None:
411
 
                    diff = None
412
 
                else:
413
 
                    diff = self._format_diff(rev, rev_id, diff_type)
 
397
                diff = self._format_diff(rev, rev_id)
414
398
                yield LogRevision(rev, revno, merge_depth, delta,
415
399
                    self.rev_tag_dict.get(rev_id), diff)
 
400
                limit = rqst.get('limit')
416
401
                if limit:
417
402
                    log_count += 1
418
403
                    if log_count >= limit:
419
404
                        return
420
405
 
421
 
    def _format_diff(self, rev, rev_id, diff_type):
 
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
422
410
        repo = self.branch.repository
423
411
        if len(rev.parent_ids) == 0:
424
412
            ancestor_id = _mod_revision.NULL_REVISION
463
451
        generate_merge_revisions = rqst.get('levels') != 1
464
452
        delayed_graph_generation = not rqst.get('specific_fileids') and (
465
453
                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'))
 
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)
472
458
 
473
459
        # Apply the other filters
474
460
        return make_log_rev_iterator(self.branch, view_revisions,
481
467
        # Note that we always generate the merge revisions because
482
468
        # filter_revisions_touching_file_id() requires them ...
483
469
        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'))
 
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'))
488
473
        if not isinstance(view_revisions, list):
489
474
            view_revisions = list(view_revisions)
490
475
        view_revisions = _filter_revisions_touching_file_id(self.branch,
495
480
 
496
481
 
497
482
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
 
                         ):
 
483
    generate_merge_revisions, allow_single_merge_revision,
 
484
    delayed_graph_generation=False):
502
485
    """Calculate the revisions to view.
503
486
 
504
487
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
505
488
             a list of the same tuples.
506
489
    """
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
490
    br_revno, br_rev_id = branch.last_revision_info()
513
491
    if br_revno == 0:
514
492
        return []
515
493
 
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)
 
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)
528
505
    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):
 
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):
538
512
    if rev_id == br_rev_id:
539
513
        # It's the tip
540
514
        return [(br_rev_id, br_revno, 0)]
541
515
    else:
542
516
        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.')
543
524
        revno_str = '.'.join(str(n) for n in revno)
544
525
        return [(rev_id, revno_str, 0)]
545
526
 
555
536
        except _StartNotLinearAncestor:
556
537
            raise errors.BzrCommandError('Start revision not found in'
557
538
                ' left-hand history of end revision.')
 
539
    if direction == 'forward':
 
540
        result = reversed(result)
558
541
    return result
559
542
 
560
543
 
561
544
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
 
                            delayed_graph_generation,
563
 
                            exclude_common_ancestry=False):
 
545
    delayed_graph_generation):
564
546
    # On large trees, generating the merge graph can take 30-60 seconds
565
547
    # so we delay doing it until a merge is detected, incrementally
566
548
    # 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
549
    initial_revisions = []
572
550
    if delayed_graph_generation:
573
551
        try:
574
 
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
552
            for rev_id, revno, depth in \
 
553
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
576
554
                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
555
                    end_rev_id = rev_id
593
556
                    break
594
557
                else:
595
558
                    initial_revisions.append((rev_id, revno, depth))
596
559
            else:
597
560
                # No merged revisions found
598
 
                return initial_revisions
 
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)
599
567
        except _StartNotLinearAncestor:
600
568
            # A merge was never detected so the lower revision limit can't
601
569
            # be nested down somewhere
602
570
            raise errors.BzrCommandError('Start revision not found in'
603
571
                ' history of end revision.')
604
572
 
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
573
    # A log including nested merges is required. If the direction is reverse,
609
574
    # we rebase the initial merge depths so that the development line is
610
575
    # shown naturally, i.e. just like it is for linear logging. We can easily
612
577
    # indented at the end seems slightly nicer in that case.
613
578
    view_revisions = chain(iter(initial_revisions),
614
579
        _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
 
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)
618
589
 
619
590
 
620
591
def _has_merges(branch, rev_id):
638
609
        else:
639
610
            # not obvious
640
611
            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
612
    return True
644
613
 
645
614
 
678
647
 
679
648
 
680
649
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
681
 
                          rebase_initial_depths=True,
682
 
                          exclude_common_ancestry=False):
 
650
    rebase_initial_depths=True):
683
651
    """Calculate revisions to view including merges, newest to oldest.
684
652
 
685
653
    :param branch: the branch
689
657
      revision is found?
690
658
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
691
659
    """
692
 
    if exclude_common_ancestry:
693
 
        stop_rule = 'with-merges-without-common-ancestry'
694
 
    else:
695
 
        stop_rule = 'with-merges'
696
660
    view_revisions = branch.iter_merge_sorted_revisions(
697
661
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
698
 
        stop_rule=stop_rule)
 
662
        stop_rule="with-merges")
699
663
    if not rebase_initial_depths:
700
664
        for (rev_id, merge_depth, revno, end_of_merge
701
665
             ) in view_revisions:
712
676
                depth_adjustment = merge_depth
713
677
            if depth_adjustment:
714
678
                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
679
                    depth_adjustment = merge_depth
720
680
                merge_depth -= depth_adjustment
721
681
            yield rev_id, '.'.join(map(str, revno)), merge_depth
722
682
 
723
683
 
724
 
@deprecated_function(deprecated_in((2, 2, 0)))
725
684
def calculate_view_revisions(branch, start_revision, end_revision, direction,
726
 
        specific_fileid, generate_merge_revisions):
 
685
        specific_fileid, generate_merge_revisions, allow_single_merge_revision):
727
686
    """Calculate the revisions to view.
728
687
 
729
688
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
730
689
             a list of the same tuples.
731
690
    """
 
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
732
694
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
733
695
        end_revision)
734
696
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
735
 
        direction, generate_merge_revisions or specific_fileid))
 
697
        direction, generate_merge_revisions or specific_fileid,
 
698
        allow_single_merge_revision))
736
699
    if specific_fileid:
737
700
        view_revisions = _filter_revisions_touching_file_id(branch,
738
701
            specific_fileid, view_revisions,
1084
1047
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1085
1048
 
1086
1049
 
1087
 
@deprecated_function(deprecated_in((2, 2, 0)))
1088
1050
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1089
1051
    """Filter view_revisions based on revision ranges.
1090
1052
 
1099
1061
 
1100
1062
    :return: The filtered view_revisions.
1101
1063
    """
 
1064
    # This method is no longer called by the main code path.
 
1065
    # It may be removed soon. IGC 20090127
1102
1066
    if start_rev_id or end_rev_id:
1103
1067
        revision_ids = [r for r, n, d in view_revisions]
1104
1068
        if start_rev_id:
1210
1174
    return result
1211
1175
 
1212
1176
 
1213
 
@deprecated_function(deprecated_in((2, 2, 0)))
1214
1177
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1215
1178
                       include_merges=True):
1216
1179
    """Produce an iterator of revisions to show
1217
1180
    :return: an iterator of (revision_id, revno, merge_depth)
1218
1181
    (if there is no revno for a revision, None is supplied)
1219
1182
    """
 
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
1220
1186
    if not include_merges:
1221
1187
        revision_ids = mainline_revs[1:]
1222
1188
        if direction == 'reverse':
1317
1283
        one (2) should be used.
1318
1284
 
1319
1285
    - 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.
 
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.
1322
1289
 
1323
1290
    - preferred_levels is the number of levels this formatter defaults to.
1324
1291
        The default value is zero meaning display all levels.
1325
1292
        This value is only relevant if supports_merge_revisions is True.
1326
1293
 
 
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
 
1327
1298
    - supports_tags must be True if this log formatter supports tags.
1328
1299
        Otherwise the tags attribute may not be populated.
1329
1300
 
1340
1311
    preferred_levels = 0
1341
1312
 
1342
1313
    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, author_list_handler=None):
 
1314
                 delta_format=None, levels=None):
1345
1315
        """Create a LogFormatter.
1346
1316
 
1347
1317
        :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
1318
        :param show_ids: if True, revision-ids are to be displayed
1351
1319
        :param show_timezone: the timezone to use
1352
1320
        :param delta_format: the level of delta information to display
1353
 
          or None to leave it to the formatter to decide
 
1321
          or None to leave it u to the formatter to decide
1354
1322
        :param levels: the number of levels to display; None or -1 to
1355
1323
          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
1360
1324
        """
1361
1325
        self.to_file = to_file
1362
1326
        # 'exact' stream used to show diff, it should print content 'as is'
1363
1327
        # and should not try to decode/encode it to unicode to avoid bug #328007
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)
 
1328
        self.to_exact_file = getattr(to_file, 'stream', to_file)
1371
1329
        self.show_ids = show_ids
1372
1330
        self.show_timezone = show_timezone
1373
1331
        if delta_format is None:
1375
1333
            delta_format = 2 # long format
1376
1334
        self.delta_format = delta_format
1377
1335
        self.levels = levels
1378
 
        self._show_advice = show_advice
1379
 
        self._merge_count = 0
1380
 
        self._author_list_handler = author_list_handler
1381
1336
 
1382
1337
    def get_levels(self):
1383
1338
        """Get the number of levels to display or 0 for all."""
1384
1339
        if getattr(self, 'supports_merge_revisions', False):
1385
1340
            if self.levels is None or self.levels == -1:
1386
 
                self.levels = self.preferred_levels
1387
 
        else:
1388
 
            self.levels = 1
1389
 
        return self.levels
 
1341
                return self.preferred_levels
 
1342
            else:
 
1343
                return self.levels
 
1344
        return 1
1390
1345
 
1391
1346
    def log_revision(self, revision):
1392
1347
        """Log a revision.
1395
1350
        """
1396
1351
        raise NotImplementedError('not implemented in abstract base')
1397
1352
 
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
 
 
1411
1353
    def short_committer(self, rev):
1412
1354
        name, address = config.parse_username(rev.committer)
1413
1355
        if name:
1415
1357
        return address
1416
1358
 
1417
1359
    def short_author(self, rev):
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 ''
 
1360
        name, address = config.parse_username(rev.get_apparent_authors()[0])
 
1361
        if name:
 
1362
            return name
 
1363
        return address
1461
1364
 
1462
1365
    def show_properties(self, revision, indent):
1463
1366
        """Displays the custom properties returned by each registered handler.
1464
1367
 
1465
1368
        If a registered handler raises an error it is propagated.
1466
1369
        """
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)
1478
1370
        for key, handler in properties_handler_registry.iteritems():
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
 
1371
            for key, value in handler(revision).items():
 
1372
                self.to_file.write(indent + key + ': ' + value + '\n')
1511
1373
 
1512
1374
    def show_diff(self, to_file, diff, indent):
1513
1375
        for l in diff.rstrip().split('\n'):
1514
1376
            to_file.write(indent + '%s\n' % (l,))
1515
1377
 
1516
1378
 
1517
 
# Separator between revisions in long format
1518
 
_LONG_SEP = '-' * 60
1519
 
 
1520
 
 
1521
1379
class LongLogFormatter(LogFormatter):
1522
1380
 
1523
1381
    supports_merge_revisions = True
1524
 
    preferred_levels = 1
1525
1382
    supports_delta = True
1526
1383
    supports_tags = True
1527
1384
    supports_diff = True
1528
1385
 
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
 
 
1544
1386
    def log_revision(self, revision):
1545
1387
        """Log a revision, either merged or not."""
1546
1388
        indent = '    ' * revision.merge_depth
1547
 
        lines = [_LONG_SEP]
 
1389
        to_file = self.to_file
 
1390
        to_file.write(indent + '-' * 60 + '\n')
1548
1391
        if revision.revno is not None:
1549
 
            lines.append('revno: %s%s' % (revision.revno,
1550
 
                self.merge_marker(revision)))
 
1392
            to_file.write(indent + 'revno: %s\n' % (revision.revno,))
1551
1393
        if revision.tags:
1552
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1394
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1553
1395
        if self.show_ids:
1554
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1396
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
 
1397
            to_file.write('\n')
1555
1398
            for parent_id in revision.rev.parent_ids:
1556
 
                lines.append('parent: %s' % (parent_id,))
1557
 
        lines.extend(self.custom_properties(revision.rev))
 
1399
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
 
1400
        self.show_properties(revision.rev, indent)
1558
1401
 
1559
1402
        committer = revision.rev.committer
1560
 
        authors = self.authors(revision.rev, 'all')
 
1403
        authors = revision.rev.get_apparent_authors()
1561
1404
        if authors != [committer]:
1562
 
            lines.append('author: %s' % (", ".join(authors),))
1563
 
        lines.append('committer: %s' % (committer,))
 
1405
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
 
1406
        to_file.write(indent + 'committer: %s\n' % (committer,))
1564
1407
 
1565
1408
        branch_nick = revision.rev.properties.get('branch-nick', None)
1566
1409
        if branch_nick is not None:
1567
 
            lines.append('branch nick: %s' % (branch_nick,))
1568
 
 
1569
 
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1570
 
 
1571
 
        lines.append('message:')
 
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')
1572
1418
        if not revision.rev.message:
1573
 
            lines.append('  (no message)')
 
1419
            to_file.write(indent + '  (no message)\n')
1574
1420
        else:
1575
1421
            message = revision.rev.message.rstrip('\r\n')
1576
1422
            for l in message.split('\n'):
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)))
 
1423
                to_file.write(indent + '  %s\n' % (l,))
1582
1424
        if revision.delta is not None:
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)
 
1425
            # We don't respect delta_format for compatibility
 
1426
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1427
                                short_status=False)
1587
1428
        if revision.diff is not None:
1588
1429
            to_file.write(indent + 'diff:\n')
1589
 
            to_file.flush()
1590
1430
            # Note: we explicitly don't indent the diff (relative to the
1591
1431
            # revision information) so that the output can be fed to patch -p0
1592
1432
            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'
1598
1433
 
1599
1434
 
1600
1435
class ShortLogFormatter(LogFormatter):
1630
1465
        offset = ' ' * (revno_width + 1)
1631
1466
 
1632
1467
        to_file = self.to_file
 
1468
        is_merge = ''
 
1469
        if len(revision.rev.parent_ids) > 1:
 
1470
            is_merge = ' [merge]'
1633
1471
        tags = ''
1634
1472
        if revision.tags:
1635
1473
            tags = ' {%s}' % (', '.join(revision.tags))
1639
1477
                            revision.rev.timezone or 0,
1640
1478
                            self.show_timezone, date_fmt="%Y-%m-%d",
1641
1479
                            show_offset=False),
1642
 
                tags, self.merge_marker(revision)))
 
1480
                tags, is_merge))
1643
1481
        self.show_properties(revision.rev, indent+offset)
1644
1482
        if self.show_ids:
1645
1483
            to_file.write(indent + offset + 'revision-id:%s\n'
1652
1490
                to_file.write(indent + offset + '%s\n' % (l,))
1653
1491
 
1654
1492
        if revision.delta is not None:
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)
 
1493
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1494
                                short_status=self.delta_format==1)
1660
1495
        if revision.diff is not None:
1661
1496
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1662
1497
        to_file.write('\n')
1670
1505
 
1671
1506
    def __init__(self, *args, **kwargs):
1672
1507
        super(LineLogFormatter, self).__init__(*args, **kwargs)
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
 
1508
        self._max_chars = terminal_width() - 1
1678
1509
 
1679
1510
    def truncate(self, str, max_len):
1680
 
        if max_len is None or len(str) <= max_len:
 
1511
        if len(str) <= max_len:
1681
1512
            return str
1682
 
        return str[:max_len-3] + '...'
 
1513
        return str[:max_len-3]+'...'
1683
1514
 
1684
1515
    def date_string(self, rev):
1685
1516
        return format_date(rev.timestamp, rev.timezone or 0,
1737
1568
                               self.show_timezone,
1738
1569
                               date_fmt='%Y-%m-%d',
1739
1570
                               show_offset=False)
1740
 
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1741
 
        committer_str = committer_str.replace(' <', '  <')
 
1571
        committer_str = revision.rev.committer.replace (' <', '  <')
1742
1572
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1743
1573
 
1744
1574
        if revision.delta is not None and revision.delta.has_changed():
1809
1639
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1810
1640
 
1811
1641
 
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
 
 
1840
1642
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1841
1643
    # deprecated; for compatibility
1842
1644
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1993
1795
        lf.log_revision(lr)
1994
1796
 
1995
1797
 
1996
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
1798
def _get_info_for_log_files(revisionspec_list, file_list):
1997
1799
    """Find file-ids and kinds given a list of files and a revision range.
1998
1800
 
1999
1801
    We search for files at the end of the range. If not found there,
2003
1805
    :param file_list: the list of paths given on the command line;
2004
1806
      the first of these can be a branch location or a file path,
2005
1807
      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.
2008
1808
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2009
1809
      info_list is a list of (relative_path, file_id, kind) tuples where
2010
1810
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2011
 
      branch will be read-locked.
2012
1811
    """
2013
1812
    from builtins import _get_revision_range, safe_relpath_files
2014
1813
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2015
 
    add_cleanup(b.lock_read().unlock)
2016
1814
    # XXX: It's damn messy converting a list of paths to relative paths when
2017
1815
    # those paths might be deleted ones, they might be on a case-insensitive
2018
1816
    # filesystem and/or they might be in silly locations (like another branch).
2028
1826
    info_list = []
2029
1827
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2030
1828
        "log")
2031
 
    if relpaths in ([], [u'']):
2032
 
        return b, [], start_rev_info, end_rev_info
2033
1829
    if start_rev_info is None and end_rev_info is None:
2034
1830
        if tree is None:
2035
1831
            tree = b.basis_tree()
2096
1892
 
2097
1893
 
2098
1894
properties_handler_registry = registry.Registry()
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)
 
1895
properties_handler_registry.register_lazy("foreign",
 
1896
                                          "bzrlib.foreign",
 
1897
                                          "show_foreign_properties")
2114
1898
 
2115
1899
 
2116
1900
# adapters which revision ids to log are filtered. When log is called, the