~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Robert Collins
  • Date: 2010-06-28 02:41:22 UTC
  • mto: This revision was merged to the branch mainline in revision 5324.
  • Revision ID: robertc@robertcollins.net-20100628024122-g951fzp74f3u6wst
Sanity check that new_trace_file in pop_log_file is valid, and also fix a test that monkey patched get_terminal_encoding.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2010 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
70
70
    diff,
71
71
    errors,
72
72
    foreign,
 
73
    osutils,
73
74
    repository as _mod_repository,
74
75
    revision as _mod_revision,
75
76
    revisionspec,
 
77
    trace,
76
78
    tsort,
77
 
    i18n,
78
79
    )
79
80
""")
80
81
 
81
82
from bzrlib import (
82
 
    lazy_regex,
83
83
    registry,
84
84
    )
85
85
from bzrlib.osutils import (
86
86
    format_date,
87
87
    format_date_with_offset_in_original_timezone,
88
 
    get_diff_header_encoding,
89
88
    get_terminal_encoding,
 
89
    re_compile_checked,
90
90
    terminal_width,
91
91
    )
92
92
from bzrlib.symbol_versioning import (
111
111
    revno = 1
112
112
    for revision_id in branch.revision_history():
113
113
        this_inv = branch.repository.get_inventory(revision_id)
114
 
        if this_inv.has_id(file_id):
 
114
        if file_id in this_inv:
115
115
            this_ie = this_inv[file_id]
116
116
            this_path = this_inv.id2path(file_id)
117
117
        else:
232
232
                          delta_type=None,
233
233
                          diff_type=None, _match_using_deltas=True,
234
234
                          exclude_common_ancestry=False,
235
 
                          signature=False,
236
235
                          ):
237
236
    """Convenience function for making a logging request dictionary.
238
237
 
262
261
      generate; 1 for just the mainline; 0 for all levels.
263
262
 
264
263
    :param generate_tags: If True, include tags for matched revisions.
265
 
`
 
264
 
266
265
    :param delta_type: Either 'full', 'partial' or None.
267
266
      'full' means generate the complete delta - adds/deletes/modifies/etc;
268
267
      'partial' means filter the delta using specific_fileids;
280
279
 
281
280
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
282
281
      range operator or as a graph difference.
283
 
 
284
 
    :param signature: show digital signature information
285
282
    """
286
283
    return {
287
284
        'direction': direction,
295
292
        'delta_type': delta_type,
296
293
        'diff_type': diff_type,
297
294
        'exclude_common_ancestry': exclude_common_ancestry,
298
 
        'signature': signature,
299
295
        # Add 'private' attributes for features that may be deprecated
300
296
        '_match_using_deltas': _match_using_deltas,
301
297
    }
303
299
 
304
300
def _apply_log_request_defaults(rqst):
305
301
    """Apply default values to a request dictionary."""
306
 
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
302
    result = _DEFAULT_REQUEST_PARAMS
307
303
    if rqst:
308
304
        result.update(rqst)
309
305
    return result
310
306
 
311
307
 
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
 
 
333
308
class LogGenerator(object):
334
309
    """A generator of log revisions."""
335
310
 
387
362
            rqst['delta_type'] = None
388
363
        if not getattr(lf, 'supports_diff', False):
389
364
            rqst['diff_type'] = None
390
 
        if not getattr(lf, 'supports_signatures', False):
391
 
            rqst['signature'] = False
392
365
 
393
366
        # Find and print the interesting revisions
394
367
        generator = self._generator_factory(self.branch, rqst)
428
401
        levels = rqst.get('levels')
429
402
        limit = rqst.get('limit')
430
403
        diff_type = rqst.get('diff_type')
431
 
        show_signature = rqst.get('signature')
432
404
        log_count = 0
433
405
        revision_iterator = self._create_log_revision_iterator()
434
406
        for revs in revision_iterator:
440
412
                    diff = None
441
413
                else:
442
414
                    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
448
415
                yield LogRevision(rev, revno, merge_depth, delta,
449
 
                    self.rev_tag_dict.get(rev_id), diff, signature)
 
416
                    self.rev_tag_dict.get(rev_id), diff)
450
417
                if limit:
451
418
                    log_count += 1
452
419
                    if log_count >= limit:
466
433
        else:
467
434
            specific_files = None
468
435
        s = StringIO()
469
 
        path_encoding = get_diff_header_encoding()
 
436
        path_encoding = osutils.get_diff_header_encoding()
470
437
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
471
438
            new_label='', path_encoding=path_encoding)
472
439
        return s.getvalue()
574
541
        # It's the tip
575
542
        return [(br_rev_id, br_revno, 0)]
576
543
    else:
577
 
        revno_str = _compute_revno_str(branch, rev_id)
 
544
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
545
        revno_str = '.'.join(str(n) for n in revno)
578
546
        return [(rev_id, revno_str, 0)]
579
547
 
580
548
 
660
628
    return len(parents) > 1
661
629
 
662
630
 
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
 
 
678
631
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
679
632
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
680
633
    if start_rev_id and 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
 
634
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
635
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
687
636
        if len(start_dotted) == 1 and len(end_dotted) == 1:
688
637
            # both on mainline
689
638
            return start_dotted[0] <= end_dotted[0]
713
662
    """
714
663
    br_revno, br_rev_id = branch.last_revision_info()
715
664
    repo = branch.repository
716
 
    graph = repo.get_graph()
717
665
    if start_rev_id is None and end_rev_id is None:
718
666
        cur_revno = br_revno
719
 
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
720
 
            (_mod_revision.NULL_REVISION,)):
 
667
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
721
668
            yield revision_id, str(cur_revno), 0
722
669
            cur_revno -= 1
723
670
    else:
724
671
        if end_rev_id is None:
725
672
            end_rev_id = br_rev_id
726
673
        found_start = start_rev_id is None
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)
 
674
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
 
675
            revno = branch.revision_id_to_dotted_revno(revision_id)
 
676
            revno_str = '.'.join(str(n) for n in revno)
730
677
            if not found_start and revision_id == start_rev_id:
731
678
                if not exclude_common_ancestry:
732
679
                    yield revision_id, revno_str, 0
864
811
    """
865
812
    if search is None:
866
813
        return log_rev_iterator
867
 
    searchRE = lazy_regex.lazy_compile(search, re.IGNORECASE)
 
814
    searchRE = re_compile_checked(search, re.IGNORECASE,
 
815
            'log message filter')
868
816
    return _filter_message_re(searchRE, log_rev_iterator)
869
817
 
870
818
 
1124
1072
    cur_revno = branch_revno
1125
1073
    rev_nos = {}
1126
1074
    mainline_revs = []
1127
 
    graph = branch.repository.get_graph()
1128
 
    for revision_id in graph.iter_lefthand_ancestry(
1129
 
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1075
    for revision_id in branch.repository.iter_reverse_revision_history(
 
1076
                        branch_last_revision):
1130
1077
        if cur_revno < start_revno:
1131
1078
            # We have gone far enough, but we always add 1 more revision
1132
1079
            rev_nos[revision_id] = cur_revno
1198
1145
    This includes the revisions which directly change the file id,
1199
1146
    and the revisions which merge these changes. So if the
1200
1147
    revision graph is::
1201
 
 
1202
1148
        A-.
1203
1149
        |\ \
1204
1150
        B C E
1231
1177
    """
1232
1178
    # Lookup all possible text keys to determine which ones actually modified
1233
1179
    # the file.
1234
 
    graph = branch.repository.get_file_graph()
1235
 
    get_parent_map = graph.get_parent_map
1236
1180
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1237
1181
    next_keys = None
1238
1182
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1242
1186
    #       indexing layer. We might consider passing in hints as to the known
1243
1187
    #       access pattern (sparse/clustered, high success rate/low success
1244
1188
    #       rate). This particular access is clustered with a low success rate.
 
1189
    get_parent_map = branch.repository.texts.get_parent_map
1245
1190
    modified_text_revisions = set()
1246
1191
    chunk_size = 1000
1247
1192
    for start in xrange(0, len(text_keys), chunk_size):
1355
1300
    """
1356
1301
 
1357
1302
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1358
 
                 tags=None, diff=None, signature=None):
 
1303
                 tags=None, diff=None):
1359
1304
        self.rev = rev
1360
 
        if revno is None:
1361
 
            self.revno = None
1362
 
        else:
1363
 
            self.revno = str(revno)
 
1305
        self.revno = str(revno)
1364
1306
        self.merge_depth = merge_depth
1365
1307
        self.delta = delta
1366
1308
        self.tags = tags
1367
1309
        self.diff = diff
1368
 
        self.signature = signature
1369
1310
 
1370
1311
 
1371
1312
class LogFormatter(object):
1380
1321
    to indicate which LogRevision attributes it supports:
1381
1322
 
1382
1323
    - supports_delta must be True if this log formatter supports delta.
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.
 
1324
        Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1325
        attribute describes whether the 'short_status' format (1) or the long
 
1326
        one (2) should be used.
1386
1327
 
1387
1328
    - supports_merge_revisions must be True if this log formatter supports
1388
 
      merge revisions.  If not, then only mainline revisions will be passed
1389
 
      to the formatter.
 
1329
        merge revisions.  If not, then only mainline revisions will be passed
 
1330
        to the formatter.
1390
1331
 
1391
1332
    - preferred_levels is the number of levels this formatter defaults to.
1392
 
      The default value is zero meaning display all levels.
1393
 
      This value is only relevant if supports_merge_revisions is True.
 
1333
        The default value is zero meaning display all levels.
 
1334
        This value is only relevant if supports_merge_revisions is True.
1394
1335
 
1395
1336
    - supports_tags must be True if this log formatter supports tags.
1396
 
      Otherwise the tags attribute may not be populated.
 
1337
        Otherwise the tags attribute may not be populated.
1397
1338
 
1398
1339
    - supports_diff must be True if this log formatter supports diffs.
1399
 
      Otherwise the diff attribute may not be populated.
1400
 
 
1401
 
    - supports_signatures must be True if this log formatter supports GPG
1402
 
      signatures.
 
1340
        Otherwise the diff attribute may not be populated.
1403
1341
 
1404
1342
    Plugins can register functions to show custom revision properties using
1405
1343
    the properties_handler_registry. The registered function
1406
 
    must respect the following interface description::
1407
 
 
 
1344
    must respect the following interface description:
1408
1345
        def my_show_properties(properties_dict):
1409
1346
            # code that returns a dict {'name':'value'} of the properties
1410
1347
            # to be shown
1597
1534
    supports_delta = True
1598
1535
    supports_tags = True
1599
1536
    supports_diff = True
1600
 
    supports_signatures = True
1601
1537
 
1602
1538
    def __init__(self, *args, **kwargs):
1603
1539
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1623
1559
                self.merge_marker(revision)))
1624
1560
        if revision.tags:
1625
1561
            lines.append('tags: %s' % (', '.join(revision.tags)))
1626
 
        if self.show_ids or revision.revno is None:
 
1562
        if self.show_ids:
1627
1563
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1628
 
        if self.show_ids:
1629
1564
            for parent_id in revision.rev.parent_ids:
1630
1565
                lines.append('parent: %s' % (parent_id,))
1631
1566
        lines.extend(self.custom_properties(revision.rev))
1642
1577
 
1643
1578
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1644
1579
 
1645
 
        if revision.signature is not None:
1646
 
            lines.append('signature: ' + revision.signature)
1647
 
 
1648
1580
        lines.append('message:')
1649
1581
        if not revision.rev.message:
1650
1582
            lines.append('  (no message)')
1697
1629
        indent = '    ' * depth
1698
1630
        revno_width = self.revno_width_by_depth.get(depth)
1699
1631
        if revno_width is None:
1700
 
            if revision.revno is None or revision.revno.find('.') == -1:
 
1632
            if revision.revno.find('.') == -1:
1701
1633
                # mainline revno, e.g. 12345
1702
1634
                revno_width = 5
1703
1635
            else:
1711
1643
        if revision.tags:
1712
1644
            tags = ' {%s}' % (', '.join(revision.tags))
1713
1645
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1714
 
                revision.revno or "", self.short_author(revision.rev),
 
1646
                revision.revno, self.short_author(revision.rev),
1715
1647
                format_date(revision.rev.timestamp,
1716
1648
                            revision.rev.timezone or 0,
1717
1649
                            self.show_timezone, date_fmt="%Y-%m-%d",
1718
1650
                            show_offset=False),
1719
1651
                tags, self.merge_marker(revision)))
1720
1652
        self.show_properties(revision.rev, indent+offset)
1721
 
        if self.show_ids or revision.revno is None:
 
1653
        if self.show_ids:
1722
1654
            to_file.write(indent + offset + 'revision-id:%s\n'
1723
1655
                          % (revision.rev.revision_id,))
1724
1656
        if not revision.rev.message:
1777
1709
 
1778
1710
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1779
1711
        """Format log info into one string. Truncate tail of 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
 
1712
        :param  revno:      revision number or None.
 
1713
                            Revision numbers counts from 1.
 
1714
        :param  rev:        revision object
 
1715
        :param  max_chars:  maximum length of resulting string
 
1716
        :param  tags:       list of tags or None
 
1717
        :param  prefix:     string to prefix each line
 
1718
        :return:            formatted truncated string
1788
1719
        """
1789
1720
        out = []
1790
1721
        if revno:
1791
1722
            # show revno only when is not None
1792
1723
            out.append("%s:" % revno)
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))
 
1724
        out.append(self.truncate(self.short_author(rev), 20))
1797
1725
        out.append(self.date_string(rev))
1798
1726
        if len(rev.parent_ids) > 1:
1799
1727
            out.append('[merge]')
1992
1920
    old_revisions = set()
1993
1921
    new_history = []
1994
1922
    new_revisions = set()
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)
 
1923
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
 
1924
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
1998
1925
    stop_revision = None
1999
1926
    do_old = True
2000
1927
    do_new = True
2092
2019
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2093
2020
      branch will be read-locked.
2094
2021
    """
2095
 
    from builtins import _get_revision_range
 
2022
    from builtins import _get_revision_range, safe_relpath_files
2096
2023
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2097
2024
    add_cleanup(b.lock_read().unlock)
2098
2025
    # XXX: It's damn messy converting a list of paths to relative paths when
2104
2031
    # case of running log in a nested directory, assuming paths beyond the
2105
2032
    # first one haven't been deleted ...
2106
2033
    if tree:
2107
 
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
2034
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2108
2035
    else:
2109
2036
        relpaths = [path] + file_list[1:]
2110
2037
    info_list = []