/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: Martin Pool
  • Date: 2011-06-28 22:39:41 UTC
  • mto: This revision was merged to the branch mainline in revision 6004.
  • Revision ID: mbp@canonical.com-20110628223941-rnxnukj963t1goft
lazy regexp tests should install it explicitly

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