~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jelmer Vernooij
  • Date: 2011-06-16 16:06:33 UTC
  • mto: This revision was merged to the branch mainline in revision 5979.
  • Revision ID: jelmer@samba.org-20110616160633-o7gytmtrm37jr89p
Fix import

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,
78
77
    )
79
78
""")
84
83
from bzrlib.osutils import (
85
84
    format_date,
86
85
    format_date_with_offset_in_original_timezone,
 
86
    get_diff_header_encoding,
87
87
    get_terminal_encoding,
88
 
    re_compile_checked,
89
88
    terminal_width,
90
89
    )
91
90
from bzrlib.symbol_versioning import (
298
297
 
299
298
def _apply_log_request_defaults(rqst):
300
299
    """Apply default values to a request dictionary."""
301
 
    result = _DEFAULT_REQUEST_PARAMS
 
300
    result = _DEFAULT_REQUEST_PARAMS.copy()
302
301
    if rqst:
303
302
        result.update(rqst)
304
303
    return result
432
431
        else:
433
432
            specific_files = None
434
433
        s = StringIO()
 
434
        path_encoding = get_diff_header_encoding()
435
435
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
436
 
            new_label='')
 
436
            new_label='', path_encoding=path_encoding)
437
437
        return s.getvalue()
438
438
 
439
439
    def _create_log_revision_iterator(self):
522
522
    elif not generate_merge_revisions:
523
523
        # If we only want to see linear revisions, we can iterate ...
524
524
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
525
 
                                             direction)
 
525
                                             direction, exclude_common_ancestry)
526
526
        if direction == 'forward':
527
527
            iter_revs = reversed(iter_revs)
528
528
    else:
539
539
        # It's the tip
540
540
        return [(br_rev_id, br_revno, 0)]
541
541
    else:
542
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
543
 
        revno_str = '.'.join(str(n) for n in revno)
 
542
        revno_str = _compute_revno_str(branch, rev_id)
544
543
        return [(rev_id, revno_str, 0)]
545
544
 
546
545
 
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)
 
546
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
 
547
                             exclude_common_ancestry=False):
 
548
    result = _linear_view_revisions(
 
549
        branch, start_rev_id, end_rev_id,
 
550
        exclude_common_ancestry=exclude_common_ancestry)
549
551
    # If a start limit was given and it's not obviously an
550
552
    # ancestor of the end limit, check it before outputting anything
551
553
    if direction == 'forward' or (start_rev_id
572
574
    if delayed_graph_generation:
573
575
        try:
574
576
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
577
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
578
                if _has_merges(branch, rev_id):
577
579
                    # The end_rev_id can be nested down somewhere. We need an
578
580
                    # explicit ancestry check. There is an ambiguity here as we
623
625
    return len(parents) > 1
624
626
 
625
627
 
 
628
def _compute_revno_str(branch, rev_id):
 
629
    """Compute the revno string from a rev_id.
 
630
 
 
631
    :return: The revno string, or None if the revision is not in the supplied
 
632
        branch.
 
633
    """
 
634
    try:
 
635
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
636
    except errors.NoSuchRevision:
 
637
        # The revision must be outside of this branch
 
638
        return None
 
639
    else:
 
640
        return '.'.join(str(n) for n in revno)
 
641
 
 
642
 
626
643
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
644
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
628
645
    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)
 
646
        try:
 
647
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
648
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
649
        except errors.NoSuchRevision:
 
650
            # one or both is not in the branch; not obvious
 
651
            return False
631
652
        if len(start_dotted) == 1 and len(end_dotted) == 1:
632
653
            # both on mainline
633
654
            return start_dotted[0] <= end_dotted[0]
643
664
    return True
644
665
 
645
666
 
646
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
667
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
668
                           exclude_common_ancestry=False):
647
669
    """Calculate a sequence of revisions to view, newest to oldest.
648
670
 
649
671
    :param start_rev_id: the lower revision-id
650
672
    :param end_rev_id: the upper revision-id
 
673
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
674
        the iterated revisions.
651
675
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
652
676
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
 
      is not found walking the left-hand history
 
677
        is not found walking the left-hand history
654
678
    """
655
679
    br_revno, br_rev_id = branch.last_revision_info()
656
680
    repo = branch.repository
 
681
    graph = repo.get_graph()
657
682
    if start_rev_id is None and end_rev_id is None:
658
683
        cur_revno = br_revno
659
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
684
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
685
            (_mod_revision.NULL_REVISION,)):
660
686
            yield revision_id, str(cur_revno), 0
661
687
            cur_revno -= 1
662
688
    else:
663
689
        if end_rev_id is None:
664
690
            end_rev_id = br_rev_id
665
691
        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)
 
692
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
693
                (_mod_revision.NULL_REVISION,)):
 
694
            revno_str = _compute_revno_str(branch, revision_id)
669
695
            if not found_start and revision_id == start_rev_id:
670
 
                yield revision_id, revno_str, 0
 
696
                if not exclude_common_ancestry:
 
697
                    yield revision_id, revno_str, 0
671
698
                found_start = True
672
699
                break
673
700
            else:
802
829
    """
803
830
    if search is None:
804
831
        return log_rev_iterator
805
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
806
 
            'log message filter')
 
832
    searchRE = re.compile(search, re.IGNORECASE)
807
833
    return _filter_message_re(searchRE, log_rev_iterator)
808
834
 
809
835
 
1063
1089
    cur_revno = branch_revno
1064
1090
    rev_nos = {}
1065
1091
    mainline_revs = []
1066
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1067
 
                        branch_last_revision):
 
1092
    graph = branch.repository.get_graph()
 
1093
    for revision_id in graph.iter_lefthand_ancestry(
 
1094
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1095
        if cur_revno < start_revno:
1069
1096
            # We have gone far enough, but we always add 1 more revision
1070
1097
            rev_nos[revision_id] = cur_revno
1136
1163
    This includes the revisions which directly change the file id,
1137
1164
    and the revisions which merge these changes. So if the
1138
1165
    revision graph is::
 
1166
 
1139
1167
        A-.
1140
1168
        |\ \
1141
1169
        B C E
1168
1196
    """
1169
1197
    # Lookup all possible text keys to determine which ones actually modified
1170
1198
    # the file.
 
1199
    graph = branch.repository.get_file_graph()
 
1200
    get_parent_map = graph.get_parent_map
1171
1201
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1202
    next_keys = None
1173
1203
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1207
    #       indexing layer. We might consider passing in hints as to the known
1178
1208
    #       access pattern (sparse/clustered, high success rate/low success
1179
1209
    #       rate). This particular access is clustered with a low success rate.
1180
 
    get_parent_map = branch.repository.texts.get_parent_map
1181
1210
    modified_text_revisions = set()
1182
1211
    chunk_size = 1000
1183
1212
    for start in xrange(0, len(text_keys), chunk_size):
1293
1322
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
1323
                 tags=None, diff=None):
1295
1324
        self.rev = rev
1296
 
        self.revno = str(revno)
 
1325
        if revno is None:
 
1326
            self.revno = None
 
1327
        else:
 
1328
            self.revno = str(revno)
1297
1329
        self.merge_depth = merge_depth
1298
1330
        self.delta = delta
1299
1331
        self.tags = tags
1312
1344
    to indicate which LogRevision attributes it supports:
1313
1345
 
1314
1346
    - 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.
 
1347
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1348
      attribute describes whether the 'short_status' format (1) or the long
 
1349
      one (2) should be used.
1318
1350
 
1319
1351
    - 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.
 
1352
      merge revisions.  If not, then only mainline revisions will be passed
 
1353
      to the formatter.
1322
1354
 
1323
1355
    - 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.
 
1356
      The default value is zero meaning display all levels.
 
1357
      This value is only relevant if supports_merge_revisions is True.
1326
1358
 
1327
1359
    - supports_tags must be True if this log formatter supports tags.
1328
 
        Otherwise the tags attribute may not be populated.
 
1360
      Otherwise the tags attribute may not be populated.
1329
1361
 
1330
1362
    - supports_diff must be True if this log formatter supports diffs.
1331
 
        Otherwise the diff attribute may not be populated.
 
1363
      Otherwise the diff attribute may not be populated.
1332
1364
 
1333
1365
    Plugins can register functions to show custom revision properties using
1334
1366
    the properties_handler_registry. The registered function
1335
 
    must respect the following interface description:
 
1367
    must respect the following interface description::
 
1368
 
1336
1369
        def my_show_properties(properties_dict):
1337
1370
            # code that returns a dict {'name':'value'} of the properties
1338
1371
            # to be shown
1341
1374
 
1342
1375
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1376
                 delta_format=None, levels=None, show_advice=False,
1344
 
                 to_exact_file=None):
 
1377
                 to_exact_file=None, author_list_handler=None):
1345
1378
        """Create a LogFormatter.
1346
1379
 
1347
1380
        :param to_file: the file to output to
1355
1388
          let the log formatter decide.
1356
1389
        :param show_advice: whether to show advice at the end of the
1357
1390
          log or not
 
1391
        :param author_list_handler: callable generating a list of
 
1392
          authors to display for a given revision
1358
1393
        """
1359
1394
        self.to_file = to_file
1360
1395
        # 'exact' stream used to show diff, it should print content 'as is'
1375
1410
        self.levels = levels
1376
1411
        self._show_advice = show_advice
1377
1412
        self._merge_count = 0
 
1413
        self._author_list_handler = author_list_handler
1378
1414
 
1379
1415
    def get_levels(self):
1380
1416
        """Get the number of levels to display or 0 for all."""
1412
1448
        return address
1413
1449
 
1414
1450
    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
 
1451
        return self.authors(rev, 'first', short=True, sep=', ')
 
1452
 
 
1453
    def authors(self, rev, who, short=False, sep=None):
 
1454
        """Generate list of authors, taking --authors option into account.
 
1455
 
 
1456
        The caller has to specify the name of a author list handler,
 
1457
        as provided by the author list registry, using the ``who``
 
1458
        argument.  That name only sets a default, though: when the
 
1459
        user selected a different author list generation using the
 
1460
        ``--authors`` command line switch, as represented by the
 
1461
        ``author_list_handler`` constructor argument, that value takes
 
1462
        precedence.
 
1463
 
 
1464
        :param rev: The revision for which to generate the list of authors.
 
1465
        :param who: Name of the default handler.
 
1466
        :param short: Whether to shorten names to either name or address.
 
1467
        :param sep: What separator to use for automatic concatenation.
 
1468
        """
 
1469
        if self._author_list_handler is not None:
 
1470
            # The user did specify --authors, which overrides the default
 
1471
            author_list_handler = self._author_list_handler
 
1472
        else:
 
1473
            # The user didn't specify --authors, so we use the caller's default
 
1474
            author_list_handler = author_list_registry.get(who)
 
1475
        names = author_list_handler(rev)
 
1476
        if short:
 
1477
            for i in range(len(names)):
 
1478
                name, address = config.parse_username(names[i])
 
1479
                if name:
 
1480
                    names[i] = name
 
1481
                else:
 
1482
                    names[i] = address
 
1483
        if sep is not None:
 
1484
            names = sep.join(names)
 
1485
        return names
1419
1486
 
1420
1487
    def merge_marker(self, revision):
1421
1488
        """Get the merge marker to include in the output or '' if none."""
1516
1583
                self.merge_marker(revision)))
1517
1584
        if revision.tags:
1518
1585
            lines.append('tags: %s' % (', '.join(revision.tags)))
1519
 
        if self.show_ids:
 
1586
        if self.show_ids or revision.revno is None:
1520
1587
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1588
        if self.show_ids:
1521
1589
            for parent_id in revision.rev.parent_ids:
1522
1590
                lines.append('parent: %s' % (parent_id,))
1523
1591
        lines.extend(self.custom_properties(revision.rev))
1524
1592
 
1525
1593
        committer = revision.rev.committer
1526
 
        authors = revision.rev.get_apparent_authors()
 
1594
        authors = self.authors(revision.rev, 'all')
1527
1595
        if authors != [committer]:
1528
1596
            lines.append('author: %s' % (", ".join(authors),))
1529
1597
        lines.append('committer: %s' % (committer,))
1586
1654
        indent = '    ' * depth
1587
1655
        revno_width = self.revno_width_by_depth.get(depth)
1588
1656
        if revno_width is None:
1589
 
            if revision.revno.find('.') == -1:
 
1657
            if revision.revno is None or revision.revno.find('.') == -1:
1590
1658
                # mainline revno, e.g. 12345
1591
1659
                revno_width = 5
1592
1660
            else:
1600
1668
        if revision.tags:
1601
1669
            tags = ' {%s}' % (', '.join(revision.tags))
1602
1670
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
 
                revision.revno, self.short_author(revision.rev),
 
1671
                revision.revno or "", self.short_author(revision.rev),
1604
1672
                format_date(revision.rev.timestamp,
1605
1673
                            revision.rev.timezone or 0,
1606
1674
                            self.show_timezone, date_fmt="%Y-%m-%d",
1607
1675
                            show_offset=False),
1608
1676
                tags, self.merge_marker(revision)))
1609
1677
        self.show_properties(revision.rev, indent+offset)
1610
 
        if self.show_ids:
 
1678
        if self.show_ids or revision.revno is None:
1611
1679
            to_file.write(indent + offset + 'revision-id:%s\n'
1612
1680
                          % (revision.rev.revision_id,))
1613
1681
        if not revision.rev.message:
1666
1734
 
1667
1735
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1736
        """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
 
1737
 
 
1738
        :param revno:      revision number or None.
 
1739
                           Revision numbers counts from 1.
 
1740
        :param rev:        revision object
 
1741
        :param max_chars:  maximum length of resulting string
 
1742
        :param tags:       list of tags or None
 
1743
        :param prefix:     string to prefix each line
 
1744
        :return:           formatted truncated string
1676
1745
        """
1677
1746
        out = []
1678
1747
        if revno:
1679
1748
            # show revno only when is not None
1680
1749
            out.append("%s:" % revno)
1681
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1750
        if max_chars is not None:
 
1751
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1752
        else:
 
1753
            out.append(self.short_author(rev))
1682
1754
        out.append(self.date_string(rev))
1683
1755
        if len(rev.parent_ids) > 1:
1684
1756
            out.append('[merge]')
1703
1775
                               self.show_timezone,
1704
1776
                               date_fmt='%Y-%m-%d',
1705
1777
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
 
1778
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1779
        committer_str = committer_str.replace(' <', '  <')
1707
1780
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1708
1781
 
1709
1782
        if revision.delta is not None and revision.delta.has_changed():
1774
1847
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1775
1848
 
1776
1849
 
 
1850
def author_list_all(rev):
 
1851
    return rev.get_apparent_authors()[:]
 
1852
 
 
1853
 
 
1854
def author_list_first(rev):
 
1855
    lst = rev.get_apparent_authors()
 
1856
    try:
 
1857
        return [lst[0]]
 
1858
    except IndexError:
 
1859
        return []
 
1860
 
 
1861
 
 
1862
def author_list_committer(rev):
 
1863
    return [rev.committer]
 
1864
 
 
1865
 
 
1866
author_list_registry = registry.Registry()
 
1867
 
 
1868
author_list_registry.register('all', author_list_all,
 
1869
                              'All authors')
 
1870
 
 
1871
author_list_registry.register('first', author_list_first,
 
1872
                              'The first author')
 
1873
 
 
1874
author_list_registry.register('committer', author_list_committer,
 
1875
                              'The committer')
 
1876
 
 
1877
 
1777
1878
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
1879
    # deprecated; for compatibility
1779
1880
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1848
1949
    old_revisions = set()
1849
1950
    new_history = []
1850
1951
    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)
 
1952
    graph = repository.get_graph()
 
1953
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1954
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1853
1955
    stop_revision = None
1854
1956
    do_old = True
1855
1957
    do_new = True
1930
2032
        lf.log_revision(lr)
1931
2033
 
1932
2034
 
1933
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2035
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2036
    """Find file-ids and kinds given a list of files and a revision range.
1935
2037
 
1936
2038
    We search for files at the end of the range. If not found there,
1940
2042
    :param file_list: the list of paths given on the command line;
1941
2043
      the first of these can be a branch location or a file path,
1942
2044
      the remainder must be file paths
 
2045
    :param add_cleanup: When the branch returned is read locked,
 
2046
      an unlock call will be queued to the cleanup.
1943
2047
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2048
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
2049
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2050
      branch will be read-locked.
1947
2051
    """
1948
 
    from builtins import _get_revision_range, safe_relpath_files
 
2052
    from builtins import _get_revision_range
1949
2053
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
 
2054
    add_cleanup(b.lock_read().unlock)
1951
2055
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
2056
    # those paths might be deleted ones, they might be on a case-insensitive
1953
2057
    # filesystem and/or they might be in silly locations (like another branch).
1957
2061
    # case of running log in a nested directory, assuming paths beyond the
1958
2062
    # first one haven't been deleted ...
1959
2063
    if tree:
1960
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2064
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1961
2065
    else:
1962
2066
        relpaths = [path] + file_list[1:]
1963
2067
    info_list = []