1
# Copyright (C) 2005-2010 Canonical Ltd
1
# Copyright (C) 2005-2011 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
84
83
from bzrlib.osutils import (
86
85
format_date_with_offset_in_original_timezone,
86
get_diff_header_encoding,
87
87
get_terminal_encoding,
91
90
from bzrlib.symbol_versioning import (
433
432
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 = 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)]
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:
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
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)
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)
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
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]
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.
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
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
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
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)
1063
1089
cur_revno = branch_revno
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
1169
1197
# Lookup all possible text keys to determine which ones actually modified
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):
1312
1344
to indicate which LogRevision attributes it supports:
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.
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
1352
merge revisions. If not, then only mainline revisions will be passed
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.
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.
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.
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::
1336
1369
def my_show_properties(properties_dict):
1337
1370
# code that returns a dict {'name':'value'} of the properties
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.
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
1391
:param author_list_handler: callable generating a list of
1392
authors to display for a given revision
1359
1394
self.to_file = to_file
1360
1395
# 'exact' stream used to show diff, it should print content 'as is'
1414
1450
def short_author(self, rev):
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1451
return self.authors(rev, 'first', short=True, sep=', ')
1453
def authors(self, rev, who, short=False, sep=None):
1454
"""Generate list of authors, taking --authors option into account.
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
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.
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
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)
1477
for i in range(len(names)):
1478
name, address = config.parse_username(names[i])
1484
names = sep.join(names)
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)))
1586
if self.show_ids or revision.revno is None:
1520
1587
lines.append('revision-id: %s' % (revision.rev.revision_id,))
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))
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
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)
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:
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
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
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))
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))
1709
1782
if revision.delta is not None and revision.delta.has_changed():
1774
1847
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1850
def author_list_all(rev):
1851
return rev.get_apparent_authors()[:]
1854
def author_list_first(rev):
1855
lst = rev.get_apparent_authors()
1862
def author_list_committer(rev):
1863
return [rev.committer]
1866
author_list_registry = registry.Registry()
1868
author_list_registry.register('all', author_list_all,
1871
author_list_registry.register('first', author_list_first,
1874
author_list_registry.register('committer', author_list_committer,
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
1930
2032
lf.log_revision(lr)
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.
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.
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])
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 ...
1960
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2064
relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1962
2066
relpaths = [path] + file_list[1:]