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
81
from bzrlib import (
84
85
from bzrlib.osutils import (
86
87
format_date_with_offset_in_original_timezone,
88
get_diff_header_encoding,
87
89
get_terminal_encoding,
91
92
from bzrlib.symbol_versioning import (
111
112
for revision_id in branch.revision_history():
112
113
this_inv = branch.repository.get_inventory(revision_id)
113
if file_id in this_inv:
114
if this_inv.has_id(file_id):
114
115
this_ie = this_inv[file_id]
115
116
this_path = this_inv.id2path(file_id)
232
233
diff_type=None, _match_using_deltas=True,
233
234
exclude_common_ancestry=False,
235
237
"""Convenience function for making a logging request dictionary.
260
262
generate; 1 for just the mainline; 0 for all levels.
262
264
:param generate_tags: If True, include tags for matched revisions.
264
266
:param delta_type: Either 'full', 'partial' or None.
265
267
'full' means generate the complete delta - adds/deletes/modifies/etc;
266
268
'partial' means filter the delta using specific_fileids;
291
295
'delta_type': delta_type,
292
296
'diff_type': diff_type,
293
297
'exclude_common_ancestry': exclude_common_ancestry,
298
'signature': signature,
294
299
# Add 'private' attributes for features that may be deprecated
295
300
'_match_using_deltas': _match_using_deltas,
299
304
def _apply_log_request_defaults(rqst):
300
305
"""Apply default values to a request dictionary."""
301
result = _DEFAULT_REQUEST_PARAMS
306
result = _DEFAULT_REQUEST_PARAMS.copy()
303
308
result.update(rqst)
312
def format_signature_validity(rev_id, repo):
313
"""get the signature validity
315
:param rev_id: revision id to validate
316
:param repo: repository of revision
317
:return: human readable string to print to log
319
from bzrlib import gpg
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"
307
333
class LogGenerator(object):
308
334
"""A generator of log revisions."""
361
387
rqst['delta_type'] = None
362
388
if not getattr(lf, 'supports_diff', False):
363
389
rqst['diff_type'] = None
390
if not getattr(lf, 'supports_signatures', False):
391
rqst['signature'] = False
365
393
# Find and print the interesting revisions
366
394
generator = self._generator_factory(self.branch, rqst)
400
428
levels = rqst.get('levels')
401
429
limit = rqst.get('limit')
402
430
diff_type = rqst.get('diff_type')
431
show_signature = rqst.get('signature')
404
433
revision_iterator = self._create_log_revision_iterator()
405
434
for revs in revision_iterator:
413
442
diff = self._format_diff(rev, rev_id, diff_type)
444
signature = format_signature_validity(rev_id,
445
self.branch.repository)
414
448
yield LogRevision(rev, revno, merge_depth, delta,
415
self.rev_tag_dict.get(rev_id), diff)
449
self.rev_tag_dict.get(rev_id), diff, signature)
418
452
if log_count >= limit:
433
467
specific_files = None
469
path_encoding = get_diff_header_encoding()
435
470
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
471
new_label='', path_encoding=path_encoding)
437
472
return s.getvalue()
439
474
def _create_log_revision_iterator(self):
522
557
elif not generate_merge_revisions:
523
558
# If we only want to see linear revisions, we can iterate ...
524
559
iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
560
direction, exclude_common_ancestry)
526
561
if direction == 'forward':
527
562
iter_revs = reversed(iter_revs)
540
575
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)
577
revno_str = _compute_revno_str(branch, rev_id)
544
578
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)
581
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
582
exclude_common_ancestry=False):
583
result = _linear_view_revisions(
584
branch, start_rev_id, end_rev_id,
585
exclude_common_ancestry=exclude_common_ancestry)
549
586
# If a start limit was given and it's not obviously an
550
587
# ancestor of the end limit, check it before outputting anything
551
588
if direction == 'forward' or (start_rev_id
572
609
if delayed_graph_generation:
574
611
for rev_id, revno, depth in _linear_view_revisions(
575
branch, start_rev_id, end_rev_id):
612
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
613
if _has_merges(branch, rev_id):
577
614
# The end_rev_id can be nested down somewhere. We need an
578
615
# explicit ancestry check. There is an ambiguity here as we
623
660
return len(parents) > 1
663
def _compute_revno_str(branch, rev_id):
664
"""Compute the revno string from a rev_id.
666
:return: The revno string, or None if the revision is not in the supplied
670
revno = branch.revision_id_to_dotted_revno(rev_id)
671
except errors.NoSuchRevision:
672
# The revision must be outside of this branch
675
return '.'.join(str(n) for n in revno)
626
678
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
679
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
628
680
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)
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
631
687
if len(start_dotted) == 1 and len(end_dotted) == 1:
632
688
# both on mainline
633
689
return start_dotted[0] <= end_dotted[0]
646
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
702
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
703
exclude_common_ancestry=False):
647
704
"""Calculate a sequence of revisions to view, newest to oldest.
649
706
:param start_rev_id: the lower revision-id
650
707
:param end_rev_id: the upper revision-id
708
:param exclude_common_ancestry: Whether the start_rev_id should be part of
709
the iterated revisions.
651
710
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
652
711
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
is not found walking the left-hand history
712
is not found walking the left-hand history
655
714
br_revno, br_rev_id = branch.last_revision_info()
656
715
repo = branch.repository
716
graph = repo.get_graph()
657
717
if start_rev_id is None and end_rev_id is None:
658
718
cur_revno = br_revno
659
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
719
for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
720
(_mod_revision.NULL_REVISION,)):
660
721
yield revision_id, str(cur_revno), 0
663
724
if end_rev_id is None:
664
725
end_rev_id = br_rev_id
665
726
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)
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)
669
730
if not found_start and revision_id == start_rev_id:
670
yield revision_id, revno_str, 0
731
if not exclude_common_ancestry:
732
yield revision_id, revno_str, 0
671
733
found_start = True
803
865
if search is None:
804
866
return log_rev_iterator
805
searchRE = re_compile_checked(search, re.IGNORECASE,
806
'log message filter')
867
searchRE = lazy_regex.lazy_compile(search, re.IGNORECASE)
807
868
return _filter_message_re(searchRE, log_rev_iterator)
1063
1124
cur_revno = branch_revno
1065
1126
mainline_revs = []
1066
for revision_id in branch.repository.iter_reverse_revision_history(
1067
branch_last_revision):
1127
graph = branch.repository.get_graph()
1128
for revision_id in graph.iter_lefthand_ancestry(
1129
branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1130
if cur_revno < start_revno:
1069
1131
# We have gone far enough, but we always add 1 more revision
1070
1132
rev_nos[revision_id] = cur_revno
1169
1232
# Lookup all possible text keys to determine which ones actually modified
1234
graph = branch.repository.get_file_graph()
1235
get_parent_map = graph.get_parent_map
1171
1236
text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1237
next_keys = None
1173
1238
# Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1242
# indexing layer. We might consider passing in hints as to the known
1178
1243
# access pattern (sparse/clustered, high success rate/low success
1179
1244
# rate). This particular access is clustered with a low success rate.
1180
get_parent_map = branch.repository.texts.get_parent_map
1181
1245
modified_text_revisions = set()
1182
1246
chunk_size = 1000
1183
1247
for start in xrange(0, len(text_keys), chunk_size):
1293
1357
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
tags=None, diff=None):
1358
tags=None, diff=None, signature=None):
1296
self.revno = str(revno)
1363
self.revno = str(revno)
1297
1364
self.merge_depth = merge_depth
1298
1365
self.delta = delta
1299
1366
self.tags = tags
1300
1367
self.diff = diff
1368
self.signature = signature
1303
1371
class LogFormatter(object):
1312
1380
to indicate which LogRevision attributes it supports:
1314
1382
- 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.
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.
1319
1387
- supports_merge_revisions must be True if this log formatter supports
1320
merge revisions. If not, then only mainline revisions will be passed
1388
merge revisions. If not, then only mainline revisions will be passed
1323
1391
- 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.
1392
The default value is zero meaning display all levels.
1393
This value is only relevant if supports_merge_revisions is True.
1327
1395
- supports_tags must be True if this log formatter supports tags.
1328
Otherwise the tags attribute may not be populated.
1396
Otherwise the tags attribute may not be populated.
1330
1398
- supports_diff must be True if this log formatter supports diffs.
1331
Otherwise the diff attribute may not be populated.
1399
Otherwise the diff attribute may not be populated.
1401
- supports_signatures must be True if this log formatter supports GPG
1333
1404
Plugins can register functions to show custom revision properties using
1334
1405
the properties_handler_registry. The registered function
1335
must respect the following interface description:
1406
must respect the following interface description::
1336
1408
def my_show_properties(properties_dict):
1337
1409
# code that returns a dict {'name':'value'} of the properties
1342
1414
def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1415
delta_format=None, levels=None, show_advice=False,
1344
to_exact_file=None):
1416
to_exact_file=None, author_list_handler=None):
1345
1417
"""Create a LogFormatter.
1347
1419
:param to_file: the file to output to
1355
1427
let the log formatter decide.
1356
1428
:param show_advice: whether to show advice at the end of the
1430
:param author_list_handler: callable generating a list of
1431
authors to display for a given revision
1359
1433
self.to_file = to_file
1360
1434
# 'exact' stream used to show diff, it should print content 'as is'
1414
1489
def short_author(self, rev):
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1490
return self.authors(rev, 'first', short=True, sep=', ')
1492
def authors(self, rev, who, short=False, sep=None):
1493
"""Generate list of authors, taking --authors option into account.
1495
The caller has to specify the name of a author list handler,
1496
as provided by the author list registry, using the ``who``
1497
argument. That name only sets a default, though: when the
1498
user selected a different author list generation using the
1499
``--authors`` command line switch, as represented by the
1500
``author_list_handler`` constructor argument, that value takes
1503
:param rev: The revision for which to generate the list of authors.
1504
:param who: Name of the default handler.
1505
:param short: Whether to shorten names to either name or address.
1506
:param sep: What separator to use for automatic concatenation.
1508
if self._author_list_handler is not None:
1509
# The user did specify --authors, which overrides the default
1510
author_list_handler = self._author_list_handler
1512
# The user didn't specify --authors, so we use the caller's default
1513
author_list_handler = author_list_registry.get(who)
1514
names = author_list_handler(rev)
1516
for i in range(len(names)):
1517
name, address = config.parse_username(names[i])
1523
names = sep.join(names)
1420
1526
def merge_marker(self, revision):
1421
1527
"""Get the merge marker to include in the output or '' if none."""
1491
1597
supports_delta = True
1492
1598
supports_tags = True
1493
1599
supports_diff = True
1600
supports_signatures = True
1495
1602
def __init__(self, *args, **kwargs):
1496
1603
super(LongLogFormatter, self).__init__(*args, **kwargs)
1516
1623
self.merge_marker(revision)))
1517
1624
if revision.tags:
1518
1625
lines.append('tags: %s' % (', '.join(revision.tags)))
1626
if self.show_ids or revision.revno is None:
1520
1627
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1521
1629
for parent_id in revision.rev.parent_ids:
1522
1630
lines.append('parent: %s' % (parent_id,))
1523
1631
lines.extend(self.custom_properties(revision.rev))
1525
1633
committer = revision.rev.committer
1526
authors = revision.rev.get_apparent_authors()
1634
authors = self.authors(revision.rev, 'all')
1527
1635
if authors != [committer]:
1528
1636
lines.append('author: %s' % (", ".join(authors),))
1529
1637
lines.append('committer: %s' % (committer,))
1535
1643
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1645
if revision.signature is not None:
1646
lines.append('signature: ' + revision.signature)
1537
1648
lines.append('message:')
1538
1649
if not revision.rev.message:
1539
1650
lines.append(' (no message)')
1586
1697
indent = ' ' * depth
1587
1698
revno_width = self.revno_width_by_depth.get(depth)
1588
1699
if revno_width is None:
1589
if revision.revno.find('.') == -1:
1700
if revision.revno is None or revision.revno.find('.') == -1:
1590
1701
# mainline revno, e.g. 12345
1591
1702
revno_width = 5
1600
1711
if revision.tags:
1601
1712
tags = ' {%s}' % (', '.join(revision.tags))
1602
1713
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
revision.revno, self.short_author(revision.rev),
1714
revision.revno or "", self.short_author(revision.rev),
1604
1715
format_date(revision.rev.timestamp,
1605
1716
revision.rev.timezone or 0,
1606
1717
self.show_timezone, date_fmt="%Y-%m-%d",
1607
1718
show_offset=False),
1608
1719
tags, self.merge_marker(revision)))
1609
1720
self.show_properties(revision.rev, indent+offset)
1721
if self.show_ids or revision.revno is None:
1611
1722
to_file.write(indent + offset + 'revision-id:%s\n'
1612
1723
% (revision.rev.revision_id,))
1613
1724
if not revision.rev.message:
1667
1778
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1779
"""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
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
1679
1791
# show revno only when is not None
1680
1792
out.append("%s:" % revno)
1681
out.append(self.truncate(self.short_author(rev), 20))
1793
if max_chars is not None:
1794
out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1796
out.append(self.short_author(rev))
1682
1797
out.append(self.date_string(rev))
1683
1798
if len(rev.parent_ids) > 1:
1684
1799
out.append('[merge]')
1703
1818
self.show_timezone,
1704
1819
date_fmt='%Y-%m-%d',
1705
1820
show_offset=False)
1706
committer_str = revision.rev.get_apparent_authors()[0].replace (' <', ' <')
1821
committer_str = self.authors(revision.rev, 'first', sep=', ')
1822
committer_str = committer_str.replace(' <', ' <')
1707
1823
to_file.write('%s %s\n\n' % (date_str,committer_str))
1709
1825
if revision.delta is not None and revision.delta.has_changed():
1774
1890
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1893
def author_list_all(rev):
1894
return rev.get_apparent_authors()[:]
1897
def author_list_first(rev):
1898
lst = rev.get_apparent_authors()
1905
def author_list_committer(rev):
1906
return [rev.committer]
1909
author_list_registry = registry.Registry()
1911
author_list_registry.register('all', author_list_all,
1914
author_list_registry.register('first', author_list_first,
1917
author_list_registry.register('committer', author_list_committer,
1777
1921
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
1922
# deprecated; for compatibility
1779
1923
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1848
1992
old_revisions = set()
1849
1993
new_history = []
1850
1994
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)
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)
1853
1998
stop_revision = None
1930
2075
lf.log_revision(lr)
1933
def _get_info_for_log_files(revisionspec_list, file_list):
2078
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2079
"""Find file-ids and kinds given a list of files and a revision range.
1936
2081
We search for files at the end of the range. If not found there,
1940
2085
:param file_list: the list of paths given on the command line;
1941
2086
the first of these can be a branch location or a file path,
1942
2087
the remainder must be file paths
2088
:param add_cleanup: When the branch returned is read locked,
2089
an unlock call will be queued to the cleanup.
1943
2090
:return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2091
info_list is a list of (relative_path, file_id, kind) tuples where
1945
2092
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2093
branch will be read-locked.
1948
from builtins import _get_revision_range, safe_relpath_files
2095
from builtins import _get_revision_range
1949
2096
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2097
add_cleanup(b.lock_read().unlock)
1951
2098
# XXX: It's damn messy converting a list of paths to relative paths when
1952
2099
# those paths might be deleted ones, they might be on a case-insensitive
1953
2100
# filesystem and/or they might be in silly locations (like another branch).
1957
2104
# case of running log in a nested directory, assuming paths beyond the
1958
2105
# first one haven't been deleted ...
1960
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2107
relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1962
2109
relpaths = [path] + file_list[1:]