~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

Move all features to bzrlib.tests.features in 2.5

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 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
73
73
    repository as _mod_repository,
74
74
    revision as _mod_revision,
75
75
    revisionspec,
76
 
    trace,
77
76
    tsort,
 
77
    i18n,
78
78
    )
79
79
""")
80
80
 
81
81
from bzrlib import (
 
82
    lazy_regex,
82
83
    registry,
83
84
    )
84
85
from bzrlib.osutils import (
85
86
    format_date,
86
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
87
89
    get_terminal_encoding,
88
 
    re_compile_checked,
89
90
    terminal_width,
90
91
    )
91
92
from bzrlib.symbol_versioning import (
110
111
    revno = 1
111
112
    for revision_id in branch.revision_history():
112
113
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if file_id in this_inv:
 
114
        if this_inv.has_id(file_id):
114
115
            this_ie = this_inv[file_id]
115
116
            this_path = this_inv.id2path(file_id)
116
117
        else:
231
232
                          delta_type=None,
232
233
                          diff_type=None, _match_using_deltas=True,
233
234
                          exclude_common_ancestry=False,
 
235
                          signature=False,
234
236
                          ):
235
237
    """Convenience function for making a logging request dictionary.
236
238
 
260
262
      generate; 1 for just the mainline; 0 for all levels.
261
263
 
262
264
    :param generate_tags: If True, include tags for matched revisions.
263
 
 
 
265
`
264
266
    :param delta_type: Either 'full', 'partial' or None.
265
267
      'full' means generate the complete delta - adds/deletes/modifies/etc;
266
268
      'partial' means filter the delta using specific_fileids;
278
280
 
279
281
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
282
      range operator or as a graph difference.
 
283
 
 
284
    :param signature: show digital signature information
281
285
    """
282
286
    return {
283
287
        'direction': direction,
291
295
        'delta_type': delta_type,
292
296
        'diff_type': diff_type,
293
297
        'exclude_common_ancestry': exclude_common_ancestry,
 
298
        'signature': signature,
294
299
        # Add 'private' attributes for features that may be deprecated
295
300
        '_match_using_deltas': _match_using_deltas,
296
301
    }
298
303
 
299
304
def _apply_log_request_defaults(rqst):
300
305
    """Apply default values to a request dictionary."""
301
 
    result = _DEFAULT_REQUEST_PARAMS
 
306
    result = _DEFAULT_REQUEST_PARAMS.copy()
302
307
    if rqst:
303
308
        result.update(rqst)
304
309
    return result
305
310
 
306
311
 
 
312
def format_signature_validity(rev_id, repo):
 
313
    """get the signature validity
 
314
    
 
315
    :param rev_id: revision id to validate
 
316
    :param repo: repository of revision
 
317
    :return: human readable string to print to log
 
318
    """
 
319
    from bzrlib import gpg
 
320
 
 
321
    gpg_strategy = gpg.GPGStrategy(None)
 
322
    result = repo.verify_revision(rev_id, gpg_strategy)
 
323
    if result[0] == gpg.SIGNATURE_VALID:
 
324
        return "valid signature from {0}".format(result[1])
 
325
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
326
        return "unknown key {0}".format(result[1])
 
327
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
328
        return "invalid signature!"
 
329
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
330
        return "no signature"
 
331
 
 
332
 
307
333
class LogGenerator(object):
308
334
    """A generator of log revisions."""
309
335
 
361
387
            rqst['delta_type'] = None
362
388
        if not getattr(lf, 'supports_diff', False):
363
389
            rqst['diff_type'] = None
 
390
        if not getattr(lf, 'supports_signatures', False):
 
391
            rqst['signature'] = False
364
392
 
365
393
        # Find and print the interesting revisions
366
394
        generator = self._generator_factory(self.branch, rqst)
400
428
        levels = rqst.get('levels')
401
429
        limit = rqst.get('limit')
402
430
        diff_type = rqst.get('diff_type')
 
431
        show_signature = rqst.get('signature')
403
432
        log_count = 0
404
433
        revision_iterator = self._create_log_revision_iterator()
405
434
        for revs in revision_iterator:
411
440
                    diff = None
412
441
                else:
413
442
                    diff = self._format_diff(rev, rev_id, diff_type)
 
443
                if show_signature:
 
444
                    signature = format_signature_validity(rev_id,
 
445
                                                self.branch.repository)
 
446
                else:
 
447
                    signature = None
414
448
                yield LogRevision(rev, revno, merge_depth, delta,
415
 
                    self.rev_tag_dict.get(rev_id), diff)
 
449
                    self.rev_tag_dict.get(rev_id), diff, signature)
416
450
                if limit:
417
451
                    log_count += 1
418
452
                    if log_count >= limit:
432
466
        else:
433
467
            specific_files = None
434
468
        s = StringIO()
 
469
        path_encoding = get_diff_header_encoding()
435
470
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
436
 
            new_label='')
 
471
            new_label='', path_encoding=path_encoding)
437
472
        return s.getvalue()
438
473
 
439
474
    def _create_log_revision_iterator(self):
522
557
    elif not generate_merge_revisions:
523
558
        # If we only want to see linear revisions, we can iterate ...
524
559
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
525
 
                                             direction)
 
560
                                             direction, exclude_common_ancestry)
526
561
        if direction == 'forward':
527
562
            iter_revs = reversed(iter_revs)
528
563
    else:
539
574
        # It's the tip
540
575
        return [(br_rev_id, br_revno, 0)]
541
576
    else:
542
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
543
 
        revno_str = '.'.join(str(n) for n in revno)
 
577
        revno_str = _compute_revno_str(branch, rev_id)
544
578
        return [(rev_id, revno_str, 0)]
545
579
 
546
580
 
547
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
548
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
 
581
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
 
582
                             exclude_common_ancestry=False):
 
583
    result = _linear_view_revisions(
 
584
        branch, start_rev_id, end_rev_id,
 
585
        exclude_common_ancestry=exclude_common_ancestry)
549
586
    # If a start limit was given and it's not obviously an
550
587
    # ancestor of the end limit, check it before outputting anything
551
588
    if direction == 'forward' or (start_rev_id
572
609
    if delayed_graph_generation:
573
610
        try:
574
611
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
612
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
613
                if _has_merges(branch, rev_id):
577
614
                    # The end_rev_id can be nested down somewhere. We need an
578
615
                    # explicit ancestry check. There is an ambiguity here as we
623
660
    return len(parents) > 1
624
661
 
625
662
 
 
663
def _compute_revno_str(branch, rev_id):
 
664
    """Compute the revno string from a rev_id.
 
665
 
 
666
    :return: The revno string, or None if the revision is not in the supplied
 
667
        branch.
 
668
    """
 
669
    try:
 
670
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
671
    except errors.NoSuchRevision:
 
672
        # The revision must be outside of this branch
 
673
        return None
 
674
    else:
 
675
        return '.'.join(str(n) for n in revno)
 
676
 
 
677
 
626
678
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
679
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
628
680
    if start_rev_id and end_rev_id:
629
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
630
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
681
        try:
 
682
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
683
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
684
        except errors.NoSuchRevision:
 
685
            # one or both is not in the branch; not obvious
 
686
            return False
631
687
        if len(start_dotted) == 1 and len(end_dotted) == 1:
632
688
            # both on mainline
633
689
            return start_dotted[0] <= end_dotted[0]
643
699
    return True
644
700
 
645
701
 
646
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
702
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
703
                           exclude_common_ancestry=False):
647
704
    """Calculate a sequence of revisions to view, newest to oldest.
648
705
 
649
706
    :param start_rev_id: the lower revision-id
650
707
    :param end_rev_id: the upper revision-id
 
708
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
709
        the iterated revisions.
651
710
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
652
711
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
 
      is not found walking the left-hand history
 
712
        is not found walking the left-hand history
654
713
    """
655
714
    br_revno, br_rev_id = branch.last_revision_info()
656
715
    repo = branch.repository
 
716
    graph = repo.get_graph()
657
717
    if start_rev_id is None and end_rev_id is None:
658
718
        cur_revno = br_revno
659
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
719
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
720
            (_mod_revision.NULL_REVISION,)):
660
721
            yield revision_id, str(cur_revno), 0
661
722
            cur_revno -= 1
662
723
    else:
663
724
        if end_rev_id is None:
664
725
            end_rev_id = br_rev_id
665
726
        found_start = start_rev_id is None
666
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
667
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
668
 
            revno_str = '.'.join(str(n) for n in revno)
 
727
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
728
                (_mod_revision.NULL_REVISION,)):
 
729
            revno_str = _compute_revno_str(branch, revision_id)
669
730
            if not found_start and revision_id == start_rev_id:
670
 
                yield revision_id, revno_str, 0
 
731
                if not exclude_common_ancestry:
 
732
                    yield revision_id, revno_str, 0
671
733
                found_start = True
672
734
                break
673
735
            else:
802
864
    """
803
865
    if search is None:
804
866
        return log_rev_iterator
805
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
806
 
            'log message filter')
 
867
    searchRE = lazy_regex.lazy_compile(search, re.IGNORECASE)
807
868
    return _filter_message_re(searchRE, log_rev_iterator)
808
869
 
809
870
 
1063
1124
    cur_revno = branch_revno
1064
1125
    rev_nos = {}
1065
1126
    mainline_revs = []
1066
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1067
 
                        branch_last_revision):
 
1127
    graph = branch.repository.get_graph()
 
1128
    for revision_id in graph.iter_lefthand_ancestry(
 
1129
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1130
        if cur_revno < start_revno:
1069
1131
            # We have gone far enough, but we always add 1 more revision
1070
1132
            rev_nos[revision_id] = cur_revno
1136
1198
    This includes the revisions which directly change the file id,
1137
1199
    and the revisions which merge these changes. So if the
1138
1200
    revision graph is::
 
1201
 
1139
1202
        A-.
1140
1203
        |\ \
1141
1204
        B C E
1168
1231
    """
1169
1232
    # Lookup all possible text keys to determine which ones actually modified
1170
1233
    # the file.
 
1234
    graph = branch.repository.get_file_graph()
 
1235
    get_parent_map = graph.get_parent_map
1171
1236
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1237
    next_keys = None
1173
1238
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1242
    #       indexing layer. We might consider passing in hints as to the known
1178
1243
    #       access pattern (sparse/clustered, high success rate/low success
1179
1244
    #       rate). This particular access is clustered with a low success rate.
1180
 
    get_parent_map = branch.repository.texts.get_parent_map
1181
1245
    modified_text_revisions = set()
1182
1246
    chunk_size = 1000
1183
1247
    for start in xrange(0, len(text_keys), chunk_size):
1291
1355
    """
1292
1356
 
1293
1357
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
 
                 tags=None, diff=None):
 
1358
                 tags=None, diff=None, signature=None):
1295
1359
        self.rev = rev
1296
 
        self.revno = str(revno)
 
1360
        if revno is None:
 
1361
            self.revno = None
 
1362
        else:
 
1363
            self.revno = str(revno)
1297
1364
        self.merge_depth = merge_depth
1298
1365
        self.delta = delta
1299
1366
        self.tags = tags
1300
1367
        self.diff = diff
 
1368
        self.signature = signature
1301
1369
 
1302
1370
 
1303
1371
class LogFormatter(object):
1312
1380
    to indicate which LogRevision attributes it supports:
1313
1381
 
1314
1382
    - supports_delta must be True if this log formatter supports delta.
1315
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1316
 
        attribute describes whether the 'short_status' format (1) or the long
1317
 
        one (2) should be used.
 
1383
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1384
      attribute describes whether the 'short_status' format (1) or the long
 
1385
      one (2) should be used.
1318
1386
 
1319
1387
    - 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.
 
1388
      merge revisions.  If not, then only mainline revisions will be passed
 
1389
      to the formatter.
1322
1390
 
1323
1391
    - preferred_levels is the number of levels this formatter defaults to.
1324
 
        The default value is zero meaning display all levels.
1325
 
        This value is only relevant if supports_merge_revisions is True.
 
1392
      The default value is zero meaning display all levels.
 
1393
      This value is only relevant if supports_merge_revisions is True.
1326
1394
 
1327
1395
    - supports_tags must be True if this log formatter supports tags.
1328
 
        Otherwise the tags attribute may not be populated.
 
1396
      Otherwise the tags attribute may not be populated.
1329
1397
 
1330
1398
    - supports_diff must be True if this log formatter supports diffs.
1331
 
        Otherwise the diff attribute may not be populated.
 
1399
      Otherwise the diff attribute may not be populated.
 
1400
 
 
1401
    - supports_signatures must be True if this log formatter supports GPG
 
1402
      signatures.
1332
1403
 
1333
1404
    Plugins can register functions to show custom revision properties using
1334
1405
    the properties_handler_registry. The registered function
1335
 
    must respect the following interface description:
 
1406
    must respect the following interface description::
 
1407
 
1336
1408
        def my_show_properties(properties_dict):
1337
1409
            # code that returns a dict {'name':'value'} of the properties
1338
1410
            # to be shown
1341
1413
 
1342
1414
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1415
                 delta_format=None, levels=None, show_advice=False,
1344
 
                 to_exact_file=None):
 
1416
                 to_exact_file=None, author_list_handler=None):
1345
1417
        """Create a LogFormatter.
1346
1418
 
1347
1419
        :param to_file: the file to output to
1355
1427
          let the log formatter decide.
1356
1428
        :param show_advice: whether to show advice at the end of the
1357
1429
          log or not
 
1430
        :param author_list_handler: callable generating a list of
 
1431
          authors to display for a given revision
1358
1432
        """
1359
1433
        self.to_file = to_file
1360
1434
        # 'exact' stream used to show diff, it should print content 'as is'
1375
1449
        self.levels = levels
1376
1450
        self._show_advice = show_advice
1377
1451
        self._merge_count = 0
 
1452
        self._author_list_handler = author_list_handler
1378
1453
 
1379
1454
    def get_levels(self):
1380
1455
        """Get the number of levels to display or 0 for all."""
1412
1487
        return address
1413
1488
 
1414
1489
    def short_author(self, rev):
1415
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1416
 
        if name:
1417
 
            return name
1418
 
        return address
 
1490
        return self.authors(rev, 'first', short=True, sep=', ')
 
1491
 
 
1492
    def authors(self, rev, who, short=False, sep=None):
 
1493
        """Generate list of authors, taking --authors option into account.
 
1494
 
 
1495
        The caller has to specify the name of a author list handler,
 
1496
        as provided by the author list registry, using the ``who``
 
1497
        argument.  That name only sets a default, though: when the
 
1498
        user selected a different author list generation using the
 
1499
        ``--authors`` command line switch, as represented by the
 
1500
        ``author_list_handler`` constructor argument, that value takes
 
1501
        precedence.
 
1502
 
 
1503
        :param rev: The revision for which to generate the list of authors.
 
1504
        :param who: Name of the default handler.
 
1505
        :param short: Whether to shorten names to either name or address.
 
1506
        :param sep: What separator to use for automatic concatenation.
 
1507
        """
 
1508
        if self._author_list_handler is not None:
 
1509
            # The user did specify --authors, which overrides the default
 
1510
            author_list_handler = self._author_list_handler
 
1511
        else:
 
1512
            # The user didn't specify --authors, so we use the caller's default
 
1513
            author_list_handler = author_list_registry.get(who)
 
1514
        names = author_list_handler(rev)
 
1515
        if short:
 
1516
            for i in range(len(names)):
 
1517
                name, address = config.parse_username(names[i])
 
1518
                if name:
 
1519
                    names[i] = name
 
1520
                else:
 
1521
                    names[i] = address
 
1522
        if sep is not None:
 
1523
            names = sep.join(names)
 
1524
        return names
1419
1525
 
1420
1526
    def merge_marker(self, revision):
1421
1527
        """Get the merge marker to include in the output or '' if none."""
1491
1597
    supports_delta = True
1492
1598
    supports_tags = True
1493
1599
    supports_diff = True
 
1600
    supports_signatures = True
1494
1601
 
1495
1602
    def __init__(self, *args, **kwargs):
1496
1603
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1516
1623
                self.merge_marker(revision)))
1517
1624
        if revision.tags:
1518
1625
            lines.append('tags: %s' % (', '.join(revision.tags)))
1519
 
        if self.show_ids:
 
1626
        if self.show_ids or revision.revno is None:
1520
1627
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1628
        if self.show_ids:
1521
1629
            for parent_id in revision.rev.parent_ids:
1522
1630
                lines.append('parent: %s' % (parent_id,))
1523
1631
        lines.extend(self.custom_properties(revision.rev))
1524
1632
 
1525
1633
        committer = revision.rev.committer
1526
 
        authors = revision.rev.get_apparent_authors()
 
1634
        authors = self.authors(revision.rev, 'all')
1527
1635
        if authors != [committer]:
1528
1636
            lines.append('author: %s' % (", ".join(authors),))
1529
1637
        lines.append('committer: %s' % (committer,))
1534
1642
 
1535
1643
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1536
1644
 
 
1645
        if revision.signature is not None:
 
1646
            lines.append('signature: ' + revision.signature)
 
1647
 
1537
1648
        lines.append('message:')
1538
1649
        if not revision.rev.message:
1539
1650
            lines.append('  (no message)')
1586
1697
        indent = '    ' * depth
1587
1698
        revno_width = self.revno_width_by_depth.get(depth)
1588
1699
        if revno_width is None:
1589
 
            if revision.revno.find('.') == -1:
 
1700
            if revision.revno is None or revision.revno.find('.') == -1:
1590
1701
                # mainline revno, e.g. 12345
1591
1702
                revno_width = 5
1592
1703
            else:
1600
1711
        if revision.tags:
1601
1712
            tags = ' {%s}' % (', '.join(revision.tags))
1602
1713
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
 
                revision.revno, self.short_author(revision.rev),
 
1714
                revision.revno or "", self.short_author(revision.rev),
1604
1715
                format_date(revision.rev.timestamp,
1605
1716
                            revision.rev.timezone or 0,
1606
1717
                            self.show_timezone, date_fmt="%Y-%m-%d",
1607
1718
                            show_offset=False),
1608
1719
                tags, self.merge_marker(revision)))
1609
1720
        self.show_properties(revision.rev, indent+offset)
1610
 
        if self.show_ids:
 
1721
        if self.show_ids or revision.revno is None:
1611
1722
            to_file.write(indent + offset + 'revision-id:%s\n'
1612
1723
                          % (revision.rev.revision_id,))
1613
1724
        if not revision.rev.message:
1666
1777
 
1667
1778
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1779
        """Format log info into one string. Truncate tail of string
1669
 
        :param  revno:      revision number or None.
1670
 
                            Revision numbers counts from 1.
1671
 
        :param  rev:        revision object
1672
 
        :param  max_chars:  maximum length of resulting string
1673
 
        :param  tags:       list of tags or None
1674
 
        :param  prefix:     string to prefix each line
1675
 
        :return:            formatted truncated string
 
1780
 
 
1781
        :param revno:      revision number or None.
 
1782
                           Revision numbers counts from 1.
 
1783
        :param rev:        revision object
 
1784
        :param max_chars:  maximum length of resulting string
 
1785
        :param tags:       list of tags or None
 
1786
        :param prefix:     string to prefix each line
 
1787
        :return:           formatted truncated string
1676
1788
        """
1677
1789
        out = []
1678
1790
        if revno:
1679
1791
            # show revno only when is not None
1680
1792
            out.append("%s:" % revno)
1681
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1793
        if max_chars is not None:
 
1794
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1795
        else:
 
1796
            out.append(self.short_author(rev))
1682
1797
        out.append(self.date_string(rev))
1683
1798
        if len(rev.parent_ids) > 1:
1684
1799
            out.append('[merge]')
1703
1818
                               self.show_timezone,
1704
1819
                               date_fmt='%Y-%m-%d',
1705
1820
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
 
1821
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1822
        committer_str = committer_str.replace(' <', '  <')
1707
1823
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1708
1824
 
1709
1825
        if revision.delta is not None and revision.delta.has_changed():
1774
1890
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1775
1891
 
1776
1892
 
 
1893
def author_list_all(rev):
 
1894
    return rev.get_apparent_authors()[:]
 
1895
 
 
1896
 
 
1897
def author_list_first(rev):
 
1898
    lst = rev.get_apparent_authors()
 
1899
    try:
 
1900
        return [lst[0]]
 
1901
    except IndexError:
 
1902
        return []
 
1903
 
 
1904
 
 
1905
def author_list_committer(rev):
 
1906
    return [rev.committer]
 
1907
 
 
1908
 
 
1909
author_list_registry = registry.Registry()
 
1910
 
 
1911
author_list_registry.register('all', author_list_all,
 
1912
                              'All authors')
 
1913
 
 
1914
author_list_registry.register('first', author_list_first,
 
1915
                              'The first author')
 
1916
 
 
1917
author_list_registry.register('committer', author_list_committer,
 
1918
                              'The committer')
 
1919
 
 
1920
 
1777
1921
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
1922
    # deprecated; for compatibility
1779
1923
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1848
1992
    old_revisions = set()
1849
1993
    new_history = []
1850
1994
    new_revisions = set()
1851
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1852
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1995
    graph = repository.get_graph()
 
1996
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1997
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1853
1998
    stop_revision = None
1854
1999
    do_old = True
1855
2000
    do_new = True
1930
2075
        lf.log_revision(lr)
1931
2076
 
1932
2077
 
1933
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2078
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2079
    """Find file-ids and kinds given a list of files and a revision range.
1935
2080
 
1936
2081
    We search for files at the end of the range. If not found there,
1940
2085
    :param file_list: the list of paths given on the command line;
1941
2086
      the first of these can be a branch location or a file path,
1942
2087
      the remainder must be file paths
 
2088
    :param add_cleanup: When the branch returned is read locked,
 
2089
      an unlock call will be queued to the cleanup.
1943
2090
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2091
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
2092
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2093
      branch will be read-locked.
1947
2094
    """
1948
 
    from builtins import _get_revision_range, safe_relpath_files
 
2095
    from builtins import _get_revision_range
1949
2096
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
 
2097
    add_cleanup(b.lock_read().unlock)
1951
2098
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
2099
    # those paths might be deleted ones, they might be on a case-insensitive
1953
2100
    # filesystem and/or they might be in silly locations (like another branch).
1957
2104
    # case of running log in a nested directory, assuming paths beyond the
1958
2105
    # first one haven't been deleted ...
1959
2106
    if tree:
1960
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2107
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1961
2108
    else:
1962
2109
        relpaths = [path] + file_list[1:]
1963
2110
    info_list = []