1
# Copyright (C) 2005-2011 Canonical Ltd
1
# Copyright (C) 2005-2010 Canonical Ltd
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
84
from bzrlib.osutils import (
85
86
format_date_with_offset_in_original_timezone,
86
get_diff_header_encoding,
87
87
get_terminal_encoding,
90
91
from bzrlib.symbol_versioning import (
432
433
specific_files = None
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='', path_encoding=path_encoding)
437
437
return s.getvalue()
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, exclude_common_ancestry)
526
526
if direction == 'forward':
527
527
iter_revs = reversed(iter_revs)
540
540
return [(br_rev_id, br_revno, 0)]
542
revno_str = _compute_revno_str(branch, rev_id)
542
revno = branch.revision_id_to_dotted_revno(rev_id)
543
revno_str = '.'.join(str(n) for n in revno)
543
544
return [(rev_id, revno_str, 0)]
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)
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)
551
549
# If a start limit was given and it's not obviously an
552
550
# ancestor of the end limit, check it before outputting anything
553
551
if direction == 'forward' or (start_rev_id
574
572
if delayed_graph_generation:
576
574
for rev_id, revno, depth in _linear_view_revisions(
577
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
575
branch, start_rev_id, end_rev_id):
578
576
if _has_merges(branch, rev_id):
579
577
# The end_rev_id can be nested down somewhere. We need an
580
578
# explicit ancestry check. There is an ambiguity here as we
625
623
return len(parents) > 1
628
def _compute_revno_str(branch, rev_id):
629
"""Compute the revno string from a rev_id.
631
:return: The revno string, or None if the revision is not in the supplied
635
revno = branch.revision_id_to_dotted_revno(rev_id)
636
except errors.NoSuchRevision:
637
# The revision must be outside of this branch
640
return '.'.join(str(n) for n in revno)
643
626
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
644
627
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
645
628
if start_rev_id and end_rev_id:
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
629
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
630
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
652
631
if len(start_dotted) == 1 and len(end_dotted) == 1:
653
632
# both on mainline
654
633
return start_dotted[0] <= end_dotted[0]
667
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
668
exclude_common_ancestry=False):
646
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
669
647
"""Calculate a sequence of revisions to view, newest to oldest.
671
649
:param start_rev_id: the lower revision-id
672
650
: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.
675
651
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
676
652
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
677
is not found walking the left-hand history
653
is not found walking the left-hand history
679
655
br_revno, br_rev_id = branch.last_revision_info()
680
656
repo = branch.repository
688
664
end_rev_id = br_rev_id
689
665
found_start = start_rev_id is None
690
666
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
691
revno_str = _compute_revno_str(branch, revision_id)
667
revno = branch.revision_id_to_dotted_revno(revision_id)
668
revno_str = '.'.join(str(n) for n in revno)
692
669
if not found_start and revision_id == start_rev_id:
693
if not exclude_common_ancestry:
694
yield revision_id, revno_str, 0
670
yield revision_id, revno_str, 0
695
671
found_start = True
827
803
if search is None:
828
804
return log_rev_iterator
829
searchRE = re.compile(search, re.IGNORECASE)
805
searchRE = re_compile_checked(search, re.IGNORECASE,
806
'log message filter')
830
807
return _filter_message_re(searchRE, log_rev_iterator)
1193
1169
# Lookup all possible text keys to determine which ones actually modified
1195
graph = branch.repository.get_file_graph()
1196
get_parent_map = graph.get_parent_map
1197
1171
text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1198
1172
next_keys = None
1199
1173
# Looking up keys in batches of 1000 can cut the time in half, as well as
1203
1177
# indexing layer. We might consider passing in hints as to the known
1204
1178
# access pattern (sparse/clustered, high success rate/low success
1205
1179
# rate). This particular access is clustered with a low success rate.
1180
get_parent_map = branch.repository.texts.get_parent_map
1206
1181
modified_text_revisions = set()
1207
1182
chunk_size = 1000
1208
1183
for start in xrange(0, len(text_keys), chunk_size):
1340
1312
to indicate which LogRevision attributes it supports:
1342
1314
- supports_delta must be True if this log formatter supports delta.
1343
Otherwise the delta attribute may not be populated. The 'delta_format'
1344
attribute describes whether the 'short_status' format (1) or the long
1345
one (2) should be used.
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
1319
- supports_merge_revisions must be True if this log formatter supports
1348
merge revisions. If not, then only mainline revisions will be passed
1320
merge revisions. If not, then only mainline revisions will be passed
1351
1323
- preferred_levels is the number of levels this formatter defaults to.
1352
The default value is zero meaning display all levels.
1353
This value is only relevant if supports_merge_revisions is True.
1324
The default value is zero meaning display all levels.
1325
This value is only relevant if supports_merge_revisions is True.
1355
1327
- supports_tags must be True if this log formatter supports tags.
1356
Otherwise the tags attribute may not be populated.
1328
Otherwise the tags attribute may not be populated.
1358
1330
- supports_diff must be True if this log formatter supports diffs.
1359
Otherwise the diff attribute may not be populated.
1331
Otherwise the diff attribute may not be populated.
1361
1333
Plugins can register functions to show custom revision properties using
1362
1334
the properties_handler_registry. The registered function
1363
must respect the following interface description::
1335
must respect the following interface description:
1365
1336
def my_show_properties(properties_dict):
1366
1337
# code that returns a dict {'name':'value'} of the properties
1371
1342
def __init__(self, to_file, show_ids=False, show_timezone='original',
1372
1343
delta_format=None, levels=None, show_advice=False,
1373
to_exact_file=None, author_list_handler=None):
1344
to_exact_file=None):
1374
1345
"""Create a LogFormatter.
1376
1347
:param to_file: the file to output to
1384
1355
let the log formatter decide.
1385
1356
:param show_advice: whether to show advice at the end of the
1387
:param author_list_handler: callable generating a list of
1388
authors to display for a given revision
1390
1359
self.to_file = to_file
1391
1360
# 'exact' stream used to show diff, it should print content 'as is'
1446
1414
def short_author(self, rev):
1447
return self.authors(rev, 'first', short=True, sep=', ')
1449
def authors(self, rev, who, short=False, sep=None):
1450
"""Generate list of authors, taking --authors option into account.
1452
The caller has to specify the name of a author list handler,
1453
as provided by the author list registry, using the ``who``
1454
argument. That name only sets a default, though: when the
1455
user selected a different author list generation using the
1456
``--authors`` command line switch, as represented by the
1457
``author_list_handler`` constructor argument, that value takes
1460
:param rev: The revision for which to generate the list of authors.
1461
:param who: Name of the default handler.
1462
:param short: Whether to shorten names to either name or address.
1463
:param sep: What separator to use for automatic concatenation.
1465
if self._author_list_handler is not None:
1466
# The user did specify --authors, which overrides the default
1467
author_list_handler = self._author_list_handler
1469
# The user didn't specify --authors, so we use the caller's default
1470
author_list_handler = author_list_registry.get(who)
1471
names = author_list_handler(rev)
1473
for i in range(len(names)):
1474
name, address = config.parse_username(names[i])
1480
names = sep.join(names)
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1483
1420
def merge_marker(self, revision):
1484
1421
"""Get the merge marker to include in the output or '' if none."""
1579
1516
self.merge_marker(revision)))
1580
1517
if revision.tags:
1581
1518
lines.append('tags: %s' % (', '.join(revision.tags)))
1582
if self.show_ids or revision.revno is None:
1583
1520
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1585
1521
for parent_id in revision.rev.parent_ids:
1586
1522
lines.append('parent: %s' % (parent_id,))
1587
1523
lines.extend(self.custom_properties(revision.rev))
1589
1525
committer = revision.rev.committer
1590
authors = self.authors(revision.rev, 'all')
1526
authors = revision.rev.get_apparent_authors()
1591
1527
if authors != [committer]:
1592
1528
lines.append('author: %s' % (", ".join(authors),))
1593
1529
lines.append('committer: %s' % (committer,))
1650
1586
indent = ' ' * depth
1651
1587
revno_width = self.revno_width_by_depth.get(depth)
1652
1588
if revno_width is None:
1653
if revision.revno is None or revision.revno.find('.') == -1:
1589
if revision.revno.find('.') == -1:
1654
1590
# mainline revno, e.g. 12345
1655
1591
revno_width = 5
1664
1600
if revision.tags:
1665
1601
tags = ' {%s}' % (', '.join(revision.tags))
1666
1602
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1667
revision.revno or "", self.short_author(revision.rev),
1603
revision.revno, self.short_author(revision.rev),
1668
1604
format_date(revision.rev.timestamp,
1669
1605
revision.rev.timezone or 0,
1670
1606
self.show_timezone, date_fmt="%Y-%m-%d",
1671
1607
show_offset=False),
1672
1608
tags, self.merge_marker(revision)))
1673
1609
self.show_properties(revision.rev, indent+offset)
1674
if self.show_ids or revision.revno is None:
1675
1611
to_file.write(indent + offset + 'revision-id:%s\n'
1676
1612
% (revision.rev.revision_id,))
1677
1613
if not revision.rev.message:
1731
1667
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1732
1668
"""Format log info into one string. Truncate tail of string
1734
:param revno: revision number or None.
1735
Revision numbers counts from 1.
1736
:param rev: revision object
1737
:param max_chars: maximum length of resulting string
1738
:param tags: list of tags or None
1739
:param prefix: string to prefix each line
1740
:return: formatted truncated 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
1744
1679
# show revno only when is not None
1745
1680
out.append("%s:" % revno)
1746
if max_chars is not None:
1747
out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1749
out.append(self.short_author(rev))
1681
out.append(self.truncate(self.short_author(rev), 20))
1750
1682
out.append(self.date_string(rev))
1751
1683
if len(rev.parent_ids) > 1:
1752
1684
out.append('[merge]')
1771
1703
self.show_timezone,
1772
1704
date_fmt='%Y-%m-%d',
1773
1705
show_offset=False)
1774
committer_str = self.authors(revision.rev, 'first', sep=', ')
1775
committer_str = committer_str.replace(' <', ' <')
1706
committer_str = revision.rev.get_apparent_authors()[0].replace (' <', ' <')
1776
1707
to_file.write('%s %s\n\n' % (date_str,committer_str))
1778
1709
if revision.delta is not None and revision.delta.has_changed():
1843
1774
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1846
def author_list_all(rev):
1847
return rev.get_apparent_authors()[:]
1850
def author_list_first(rev):
1851
lst = rev.get_apparent_authors()
1858
def author_list_committer(rev):
1859
return [rev.committer]
1862
author_list_registry = registry.Registry()
1864
author_list_registry.register('all', author_list_all,
1867
author_list_registry.register('first', author_list_first,
1870
author_list_registry.register('committer', author_list_committer,
1874
1777
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1875
1778
# deprecated; for compatibility
1876
1779
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
2027
1930
lf.log_revision(lr)
2030
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1933
def _get_info_for_log_files(revisionspec_list, file_list):
2031
1934
"""Find file-ids and kinds given a list of files and a revision range.
2033
1936
We search for files at the end of the range. If not found there,
2037
1940
:param file_list: the list of paths given on the command line;
2038
1941
the first of these can be a branch location or a file path,
2039
1942
the remainder must be file paths
2040
:param add_cleanup: When the branch returned is read locked,
2041
an unlock call will be queued to the cleanup.
2042
1943
:return: (branch, info_list, start_rev_info, end_rev_info) where
2043
1944
info_list is a list of (relative_path, file_id, kind) tuples where
2044
1945
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2045
1946
branch will be read-locked.
2047
from builtins import _get_revision_range
1948
from builtins import _get_revision_range, safe_relpath_files
2048
1949
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2049
add_cleanup(b.lock_read().unlock)
2050
1951
# XXX: It's damn messy converting a list of paths to relative paths when
2051
1952
# those paths might be deleted ones, they might be on a case-insensitive
2052
1953
# filesystem and/or they might be in silly locations (like another branch).
2056
1957
# case of running log in a nested directory, assuming paths beyond the
2057
1958
# first one haven't been deleted ...
2059
relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1960
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2061
1962
relpaths = [path] + file_list[1:]