~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: John Arbash Meinel
  • Date: 2010-02-17 17:11:16 UTC
  • mfrom: (4797.2.17 2.1)
  • mto: (4797.2.18 2.1)
  • mto: This revision was merged to the branch mainline in revision 5055.
  • Revision ID: john@arbash-meinel.com-20100217171116-h7t9223ystbnx5h8
merge bzr.2.1 in preparation for NEWS entry.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2009 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
83
83
    )
84
84
from bzrlib.osutils import (
85
85
    format_date,
 
86
    format_date_with_offset_in_original_timezone,
86
87
    get_terminal_encoding,
87
88
    re_compile_checked,
88
89
    terminal_width,
384
385
        :return: An iterator yielding LogRevision objects.
385
386
        """
386
387
        rqst = self.rqst
 
388
        levels = rqst.get('levels')
 
389
        limit = rqst.get('limit')
 
390
        diff_type = rqst.get('diff_type')
387
391
        log_count = 0
388
392
        revision_iterator = self._create_log_revision_iterator()
389
393
        for revs in revision_iterator:
390
394
            for (rev_id, revno, merge_depth), rev, delta in revs:
391
395
                # 0 levels means show everything; merge_depth counts from 0
392
 
                levels = rqst.get('levels')
393
396
                if levels != 0 and merge_depth >= levels:
394
397
                    continue
395
 
                diff = self._format_diff(rev, rev_id)
 
398
                if diff_type is None:
 
399
                    diff = None
 
400
                else:
 
401
                    diff = self._format_diff(rev, rev_id, diff_type)
396
402
                yield LogRevision(rev, revno, merge_depth, delta,
397
403
                    self.rev_tag_dict.get(rev_id), diff)
398
 
                limit = rqst.get('limit')
399
404
                if limit:
400
405
                    log_count += 1
401
406
                    if log_count >= limit:
402
407
                        return
403
408
 
404
 
    def _format_diff(self, rev, rev_id):
405
 
        diff_type = self.rqst.get('diff_type')
406
 
        if diff_type is None:
407
 
            return None
 
409
    def _format_diff(self, rev, rev_id, diff_type):
408
410
        repo = self.branch.repository
409
411
        if len(rev.parent_ids) == 0:
410
412
            ancestor_id = _mod_revision.NULL_REVISION
1291
1293
    preferred_levels = 0
1292
1294
 
1293
1295
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1294
 
                 delta_format=None, levels=None, show_advice=False):
 
1296
            delta_format=None, levels=None, show_advice=False,
 
1297
            to_exact_file=None):
1295
1298
        """Create a LogFormatter.
1296
1299
 
1297
1300
        :param to_file: the file to output to
 
1301
        :param to_exact_file: if set, gives an output stream to which 
 
1302
             non-Unicode diffs are written.
1298
1303
        :param show_ids: if True, revision-ids are to be displayed
1299
1304
        :param show_timezone: the timezone to use
1300
1305
        :param delta_format: the level of delta information to display
1307
1312
        self.to_file = to_file
1308
1313
        # 'exact' stream used to show diff, it should print content 'as is'
1309
1314
        # and should not try to decode/encode it to unicode to avoid bug #328007
1310
 
        self.to_exact_file = getattr(to_file, 'stream', to_file)
 
1315
        if to_exact_file is not None:
 
1316
            self.to_exact_file = to_exact_file
 
1317
        else:
 
1318
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1319
            # for code that expects to get diffs to pass in the exact file
 
1320
            # stream
 
1321
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1311
1322
        self.show_ids = show_ids
1312
1323
        self.show_timezone = show_timezone
1313
1324
        if delta_format is None:
1367
1378
        else:
1368
1379
            return ''
1369
1380
 
1370
 
    def show_foreign_info(self, rev, indent):
 
1381
    def show_properties(self, revision, indent):
 
1382
        """Displays the custom properties returned by each registered handler.
 
1383
 
 
1384
        If a registered handler raises an error it is propagated.
 
1385
        """
 
1386
        for line in self.custom_properties(revision):
 
1387
            self.to_file.write("%s%s\n" % (indent, line))
 
1388
 
 
1389
    def custom_properties(self, revision):
 
1390
        """Format the custom properties returned by each registered handler.
 
1391
 
 
1392
        If a registered handler raises an error it is propagated.
 
1393
 
 
1394
        :return: a list of formatted lines (excluding trailing newlines)
 
1395
        """
 
1396
        lines = self._foreign_info_properties(revision)
 
1397
        for key, handler in properties_handler_registry.iteritems():
 
1398
            lines.extend(self._format_properties(handler(revision)))
 
1399
        return lines
 
1400
 
 
1401
    def _foreign_info_properties(self, rev):
1371
1402
        """Custom log displayer for foreign revision identifiers.
1372
1403
 
1373
1404
        :param rev: Revision object.
1374
1405
        """
1375
1406
        # Revision comes directly from a foreign repository
1376
1407
        if isinstance(rev, foreign.ForeignRevision):
1377
 
            self._write_properties(indent, rev.mapping.vcs.show_foreign_revid(
1378
 
                rev.foreign_revid))
1379
 
            return
 
1408
            return self._format_properties(rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1380
1409
 
1381
1410
        # Imported foreign revision revision ids always contain :
1382
1411
        if not ":" in rev.revision_id:
1383
 
            return
 
1412
            return []
1384
1413
 
1385
1414
        # Revision was once imported from a foreign repository
1386
1415
        try:
1387
1416
            foreign_revid, mapping = \
1388
1417
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1389
1418
        except errors.InvalidRevisionId:
1390
 
            return
 
1419
            return []
1391
1420
 
1392
 
        self._write_properties(indent, 
 
1421
        return self._format_properties(
1393
1422
            mapping.vcs.show_foreign_revid(foreign_revid))
1394
1423
 
1395
 
    def show_properties(self, revision, indent):
1396
 
        """Displays the custom properties returned by each registered handler.
1397
 
 
1398
 
        If a registered handler raises an error it is propagated.
1399
 
        """
1400
 
        for key, handler in properties_handler_registry.iteritems():
1401
 
            self._write_properties(indent, handler(revision))
1402
 
 
1403
 
    def _write_properties(self, indent, properties):
 
1424
    def _format_properties(self, properties):
 
1425
        lines = []
1404
1426
        for key, value in properties.items():
1405
 
            self.to_file.write(indent + key + ': ' + value + '\n')
 
1427
            lines.append(key + ': ' + value)
 
1428
        return lines
1406
1429
 
1407
1430
    def show_diff(self, to_file, diff, indent):
1408
1431
        for l in diff.rstrip().split('\n'):
1409
1432
            to_file.write(indent + '%s\n' % (l,))
1410
1433
 
1411
1434
 
 
1435
# Separator between revisions in long format
 
1436
_LONG_SEP = '-' * 60
 
1437
 
 
1438
 
1412
1439
class LongLogFormatter(LogFormatter):
1413
1440
 
1414
1441
    supports_merge_revisions = True
1417
1444
    supports_tags = True
1418
1445
    supports_diff = True
1419
1446
 
 
1447
    def __init__(self, *args, **kwargs):
 
1448
        super(LongLogFormatter, self).__init__(*args, **kwargs)
 
1449
        if self.show_timezone == 'original':
 
1450
            self.date_string = self._date_string_original_timezone
 
1451
        else:
 
1452
            self.date_string = self._date_string_with_timezone
 
1453
 
 
1454
    def _date_string_with_timezone(self, rev):
 
1455
        return format_date(rev.timestamp, rev.timezone or 0,
 
1456
                           self.show_timezone)
 
1457
 
 
1458
    def _date_string_original_timezone(self, rev):
 
1459
        return format_date_with_offset_in_original_timezone(rev.timestamp,
 
1460
            rev.timezone or 0)
 
1461
 
1420
1462
    def log_revision(self, revision):
1421
1463
        """Log a revision, either merged or not."""
1422
1464
        indent = '    ' * revision.merge_depth
1423
 
        to_file = self.to_file
1424
 
        to_file.write(indent + '-' * 60 + '\n')
 
1465
        lines = [_LONG_SEP]
1425
1466
        if revision.revno is not None:
1426
 
            to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
 
1467
            lines.append('revno: %s%s' % (revision.revno,
1427
1468
                self.merge_marker(revision)))
1428
1469
        if revision.tags:
1429
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
 
1470
            lines.append('tags: %s' % (', '.join(revision.tags)))
1430
1471
        if self.show_ids:
1431
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1432
 
            to_file.write('\n')
 
1472
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1433
1473
            for parent_id in revision.rev.parent_ids:
1434
 
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
1435
 
        self.show_foreign_info(revision.rev, indent)
1436
 
        self.show_properties(revision.rev, indent)
 
1474
                lines.append('parent: %s' % (parent_id,))
 
1475
        lines.extend(self.custom_properties(revision.rev))
1437
1476
 
1438
1477
        committer = revision.rev.committer
1439
1478
        authors = revision.rev.get_apparent_authors()
1440
1479
        if authors != [committer]:
1441
 
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1442
 
        to_file.write(indent + 'committer: %s\n' % (committer,))
 
1480
            lines.append('author: %s' % (", ".join(authors),))
 
1481
        lines.append('committer: %s' % (committer,))
1443
1482
 
1444
1483
        branch_nick = revision.rev.properties.get('branch-nick', None)
1445
1484
        if branch_nick is not None:
1446
 
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
1447
 
 
1448
 
        date_str = format_date(revision.rev.timestamp,
1449
 
                               revision.rev.timezone or 0,
1450
 
                               self.show_timezone)
1451
 
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
1452
 
 
1453
 
        to_file.write(indent + 'message:\n')
 
1485
            lines.append('branch nick: %s' % (branch_nick,))
 
1486
 
 
1487
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1488
 
 
1489
        lines.append('message:')
1454
1490
        if not revision.rev.message:
1455
 
            to_file.write(indent + '  (no message)\n')
 
1491
            lines.append('  (no message)')
1456
1492
        else:
1457
1493
            message = revision.rev.message.rstrip('\r\n')
1458
1494
            for l in message.split('\n'):
1459
 
                to_file.write(indent + '  %s\n' % (l,))
 
1495
                lines.append('  %s' % (l,))
 
1496
 
 
1497
        # Dump the output, appending the delta and diff if requested
 
1498
        to_file = self.to_file
 
1499
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1460
1500
        if revision.delta is not None:
1461
1501
            # We don't respect delta_format for compatibility
1462
1502
            revision.delta.show(to_file, self.show_ids, indent=indent,
1463
1503
                                short_status=False)
1464
1504
        if revision.diff is not None:
1465
1505
            to_file.write(indent + 'diff:\n')
 
1506
            to_file.flush()
1466
1507
            # Note: we explicitly don't indent the diff (relative to the
1467
1508
            # revision information) so that the output can be fed to patch -p0
1468
1509
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1510
            self.to_exact_file.flush()
1469
1511
 
1470
1512
    def get_advice_separator(self):
1471
1513
        """Get the text separating the log from the closing advice."""
1515
1557
                            self.show_timezone, date_fmt="%Y-%m-%d",
1516
1558
                            show_offset=False),
1517
1559
                tags, self.merge_marker(revision)))
1518
 
        self.show_foreign_info(revision.rev, indent+offset)
1519
1560
        self.show_properties(revision.rev, indent+offset)
1520
1561
        if self.show_ids:
1521
1562
            to_file.write(indent + offset + 'revision-id:%s\n'
1543
1584
 
1544
1585
    def __init__(self, *args, **kwargs):
1545
1586
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1546
 
        self._max_chars = terminal_width() - 1
 
1587
        width = terminal_width()
 
1588
        if width is not None:
 
1589
            # we need one extra space for terminals that wrap on last char
 
1590
            width = width - 1
 
1591
        self._max_chars = width
1547
1592
 
1548
1593
    def truncate(self, str, max_len):
1549
 
        if len(str) <= max_len:
 
1594
        if max_len is None or len(str) <= max_len:
1550
1595
            return str
1551
 
        return str[:max_len-3]+'...'
 
1596
        return str[:max_len-3] + '...'
1552
1597
 
1553
1598
    def date_string(self, rev):
1554
1599
        return format_date(rev.timestamp, rev.timezone or 0,
1846
1891
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1847
1892
      info_list is a list of (relative_path, file_id, kind) tuples where
1848
1893
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
1894
      branch will be read-locked.
1849
1895
    """
1850
1896
    from builtins import _get_revision_range, safe_relpath_files
1851
1897
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
1898
    b.lock_read()
1852
1899
    # XXX: It's damn messy converting a list of paths to relative paths when
1853
1900
    # those paths might be deleted ones, they might be on a case-insensitive
1854
1901
    # filesystem and/or they might be in silly locations (like another branch).
1933
1980
 
1934
1981
properties_handler_registry = registry.Registry()
1935
1982
 
 
1983
# Use the properties handlers to print out bug information if available
 
1984
def _bugs_properties_handler(revision):
 
1985
    if revision.properties.has_key('bugs'):
 
1986
        bug_lines = revision.properties['bugs'].split('\n')
 
1987
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
1988
        fixed_bug_urls = [row[0] for row in bug_rows if
 
1989
                          len(row) > 1 and row[1] == 'fixed']
 
1990
        
 
1991
        if fixed_bug_urls:
 
1992
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
1993
    return {}
 
1994
 
 
1995
properties_handler_registry.register('bugs_properties_handler',
 
1996
                                     _bugs_properties_handler)
 
1997
 
1936
1998
 
1937
1999
# adapters which revision ids to log are filtered. When log is called, the
1938
2000
# log_rev_iterator is adapted through each of these factory methods.