/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: Jonathan Riddell
  • Date: 2011-06-30 14:36:05 UTC
  • mto: This revision was merged to the branch mainline in revision 6003.
  • Revision ID: jriddell@canonical.com-20110630143605-79a4itggyxaj2r2k
gpg.py uses i18n but bzrlib.i18n is not ready for use, so add a stub class for now

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