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
73
73
repository as _mod_repository,
74
74
revision as _mod_revision,
81
80
from bzrlib import (
84
84
from bzrlib.osutils import (
86
86
format_date_with_offset_in_original_timezone,
87
get_diff_header_encoding,
87
88
get_terminal_encoding,
91
91
from bzrlib.symbol_versioning import (
433
433
specific_files = None
435
path_encoding = get_diff_header_encoding()
435
436
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
437
new_label='', path_encoding=path_encoding)
437
438
return s.getvalue()
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,
526
direction, exclude_common_ancestry)
526
527
if direction == 'forward':
527
528
iter_revs = reversed(iter_revs)
540
541
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)
543
revno_str = _compute_revno_str(branch, rev_id)
544
544
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)
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:
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
629
def _compute_revno_str(branch, rev_id):
630
"""Compute the revno string from a rev_id.
632
:return: The revno string, or None if the revision is not in the supplied
636
revno = branch.revision_id_to_dotted_revno(rev_id)
637
except errors.NoSuchRevision:
638
# The revision must be outside of this branch
641
return '.'.join(str(n) for n in revno)
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)
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
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]
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.
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
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
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)
1169
1194
# Lookup all possible text keys to determine which ones actually modified
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):
1312
1341
to indicate which LogRevision attributes it supports:
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.
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
1349
merge revisions. If not, then only mainline revisions will be passed
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.
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.
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.
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::
1336
1366
def my_show_properties(properties_dict):
1337
1367
# code that returns a dict {'name':'value'} of the properties
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.
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
1388
:param author_list_handler: callable generating a list of
1389
authors to display for a given revision
1359
1391
self.to_file = to_file
1360
1392
# 'exact' stream used to show diff, it should print content 'as is'
1414
1447
def short_author(self, rev):
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1448
return self.authors(rev, 'first', short=True, sep=', ')
1450
def authors(self, rev, who, short=False, sep=None):
1451
"""Generate list of authors, taking --authors option into account.
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
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.
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
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)
1474
for i in range(len(names)):
1475
name, address = config.parse_username(names[i])
1481
names = sep.join(names)
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)))
1583
if self.show_ids or revision.revno is None:
1520
1584
lines.append('revision-id: %s' % (revision.rev.revision_id,))
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))
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
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)
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:
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
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
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))
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))
1709
1779
if revision.delta is not None and revision.delta.has_changed():
1774
1844
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1847
def author_list_all(rev):
1848
return rev.get_apparent_authors()[:]
1851
def author_list_first(rev):
1852
lst = rev.get_apparent_authors()
1859
def author_list_committer(rev):
1860
return [rev.committer]
1863
author_list_registry = registry.Registry()
1865
author_list_registry.register('all', author_list_all,
1868
author_list_registry.register('first', author_list_first,
1871
author_list_registry.register('committer', author_list_committer,
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)
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.
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.
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])
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 ...
1960
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2060
relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1962
2062
relpaths = [path] + file_list[1:]