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
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 (
111
111
for revision_id in branch.revision_history():
112
112
this_inv = branch.repository.get_inventory(revision_id)
113
if file_id in this_inv:
113
if this_inv.has_id(file_id):
114
114
this_ie = this_inv[file_id]
115
115
this_path = this_inv.id2path(file_id)
232
232
diff_type=None, _match_using_deltas=True,
233
233
exclude_common_ancestry=False,
235
236
"""Convenience function for making a logging request dictionary.
260
261
generate; 1 for just the mainline; 0 for all levels.
262
263
:param generate_tags: If True, include tags for matched revisions.
264
265
:param delta_type: Either 'full', 'partial' or None.
265
266
'full' means generate the complete delta - adds/deletes/modifies/etc;
266
267
'partial' means filter the delta using specific_fileids;
291
294
'delta_type': delta_type,
292
295
'diff_type': diff_type,
293
296
'exclude_common_ancestry': exclude_common_ancestry,
297
'signature': signature,
294
298
# Add 'private' attributes for features that may be deprecated
295
299
'_match_using_deltas': _match_using_deltas,
299
303
def _apply_log_request_defaults(rqst):
300
304
"""Apply default values to a request dictionary."""
301
result = _DEFAULT_REQUEST_PARAMS
305
result = _DEFAULT_REQUEST_PARAMS.copy()
303
307
result.update(rqst)
311
def format_signature_validity(rev_id, repo):
312
"""get the signature validity
314
:param rev_id: revision id to validate
315
:param repo: repository of revision
316
:return: human readable string to print to log
318
from bzrlib import gpg
320
gpg_strategy = gpg.GPGStrategy(None)
321
result = repo.verify_revision(rev_id, gpg_strategy)
322
if result[0] == gpg.SIGNATURE_VALID:
323
return "valid signature from {0}".format(result[1])
324
if result[0] == gpg.SIGNATURE_KEY_MISSING:
325
return "unknown key {0}".format(result[1])
326
if result[0] == gpg.SIGNATURE_NOT_VALID:
327
return "invalid signature!"
328
if result[0] == gpg.SIGNATURE_NOT_SIGNED:
329
return "no signature"
307
332
class LogGenerator(object):
308
333
"""A generator of log revisions."""
361
386
rqst['delta_type'] = None
362
387
if not getattr(lf, 'supports_diff', False):
363
388
rqst['diff_type'] = None
389
if not getattr(lf, 'supports_signatures', False):
390
rqst['signature'] = False
365
392
# Find and print the interesting revisions
366
393
generator = self._generator_factory(self.branch, rqst)
400
427
levels = rqst.get('levels')
401
428
limit = rqst.get('limit')
402
429
diff_type = rqst.get('diff_type')
430
show_signature = rqst.get('signature')
404
432
revision_iterator = self._create_log_revision_iterator()
405
433
for revs in revision_iterator:
413
441
diff = self._format_diff(rev, rev_id, diff_type)
443
signature = format_signature_validity(rev_id,
444
self.branch.repository)
414
447
yield LogRevision(rev, revno, merge_depth, delta,
415
self.rev_tag_dict.get(rev_id), diff)
448
self.rev_tag_dict.get(rev_id), diff, signature)
418
451
if log_count >= limit:
433
466
specific_files = None
468
path_encoding = get_diff_header_encoding()
435
469
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
470
new_label='', path_encoding=path_encoding)
437
471
return s.getvalue()
439
473
def _create_log_revision_iterator(self):
522
556
elif not generate_merge_revisions:
523
557
# If we only want to see linear revisions, we can iterate ...
524
558
iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
559
direction, exclude_common_ancestry)
526
560
if direction == 'forward':
527
561
iter_revs = reversed(iter_revs)
540
574
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)
576
revno_str = _compute_revno_str(branch, rev_id)
544
577
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)
580
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
581
exclude_common_ancestry=False):
582
result = _linear_view_revisions(
583
branch, start_rev_id, end_rev_id,
584
exclude_common_ancestry=exclude_common_ancestry)
549
585
# If a start limit was given and it's not obviously an
550
586
# ancestor of the end limit, check it before outputting anything
551
587
if direction == 'forward' or (start_rev_id
572
608
if delayed_graph_generation:
574
610
for rev_id, revno, depth in _linear_view_revisions(
575
branch, start_rev_id, end_rev_id):
611
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
612
if _has_merges(branch, rev_id):
577
613
# The end_rev_id can be nested down somewhere. We need an
578
614
# explicit ancestry check. There is an ambiguity here as we
623
659
return len(parents) > 1
662
def _compute_revno_str(branch, rev_id):
663
"""Compute the revno string from a rev_id.
665
:return: The revno string, or None if the revision is not in the supplied
669
revno = branch.revision_id_to_dotted_revno(rev_id)
670
except errors.NoSuchRevision:
671
# The revision must be outside of this branch
674
return '.'.join(str(n) for n in revno)
626
677
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
678
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
628
679
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)
681
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
682
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
683
except errors.NoSuchRevision:
684
# one or both is not in the branch; not obvious
631
686
if len(start_dotted) == 1 and len(end_dotted) == 1:
632
687
# both on mainline
633
688
return start_dotted[0] <= end_dotted[0]
646
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
701
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
702
exclude_common_ancestry=False):
647
703
"""Calculate a sequence of revisions to view, newest to oldest.
649
705
:param start_rev_id: the lower revision-id
650
706
:param end_rev_id: the upper revision-id
707
:param exclude_common_ancestry: Whether the start_rev_id should be part of
708
the iterated revisions.
651
709
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
652
710
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
is not found walking the left-hand history
711
is not found walking the left-hand history
655
713
br_revno, br_rev_id = branch.last_revision_info()
656
714
repo = branch.repository
715
graph = repo.get_graph()
657
716
if start_rev_id is None and end_rev_id is None:
658
717
cur_revno = br_revno
659
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
718
for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
719
(_mod_revision.NULL_REVISION,)):
660
720
yield revision_id, str(cur_revno), 0
663
723
if end_rev_id is None:
664
724
end_rev_id = br_rev_id
665
725
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)
726
for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
727
(_mod_revision.NULL_REVISION,)):
728
revno_str = _compute_revno_str(branch, revision_id)
669
729
if not found_start and revision_id == start_rev_id:
670
yield revision_id, revno_str, 0
730
if not exclude_common_ancestry:
731
yield revision_id, revno_str, 0
671
732
found_start = True
803
864
if search is None:
804
865
return log_rev_iterator
805
searchRE = re_compile_checked(search, re.IGNORECASE,
806
'log message filter')
866
searchRE = re.compile(search, re.IGNORECASE)
807
867
return _filter_message_re(searchRE, log_rev_iterator)
1063
1123
cur_revno = branch_revno
1065
1125
mainline_revs = []
1066
for revision_id in branch.repository.iter_reverse_revision_history(
1067
branch_last_revision):
1126
graph = branch.repository.get_graph()
1127
for revision_id in graph.iter_lefthand_ancestry(
1128
branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1129
if cur_revno < start_revno:
1069
1130
# We have gone far enough, but we always add 1 more revision
1070
1131
rev_nos[revision_id] = cur_revno
1169
1231
# Lookup all possible text keys to determine which ones actually modified
1233
graph = branch.repository.get_file_graph()
1234
get_parent_map = graph.get_parent_map
1171
1235
text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1236
next_keys = None
1173
1237
# Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1241
# indexing layer. We might consider passing in hints as to the known
1178
1242
# access pattern (sparse/clustered, high success rate/low success
1179
1243
# rate). This particular access is clustered with a low success rate.
1180
get_parent_map = branch.repository.texts.get_parent_map
1181
1244
modified_text_revisions = set()
1182
1245
chunk_size = 1000
1183
1246
for start in xrange(0, len(text_keys), chunk_size):
1293
1356
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
tags=None, diff=None):
1357
tags=None, diff=None, signature=None):
1296
self.revno = str(revno)
1362
self.revno = str(revno)
1297
1363
self.merge_depth = merge_depth
1298
1364
self.delta = delta
1299
1365
self.tags = tags
1300
1366
self.diff = diff
1367
self.signature = signature
1303
1370
class LogFormatter(object):
1312
1379
to indicate which LogRevision attributes it supports:
1314
1381
- 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.
1382
Otherwise the delta attribute may not be populated. The 'delta_format'
1383
attribute describes whether the 'short_status' format (1) or the long
1384
one (2) should be used.
1319
1386
- supports_merge_revisions must be True if this log formatter supports
1320
merge revisions. If not, then only mainline revisions will be passed
1387
merge revisions. If not, then only mainline revisions will be passed
1323
1390
- 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.
1391
The default value is zero meaning display all levels.
1392
This value is only relevant if supports_merge_revisions is True.
1327
1394
- supports_tags must be True if this log formatter supports tags.
1328
Otherwise the tags attribute may not be populated.
1395
Otherwise the tags attribute may not be populated.
1330
1397
- supports_diff must be True if this log formatter supports diffs.
1331
Otherwise the diff attribute may not be populated.
1398
Otherwise the diff attribute may not be populated.
1400
- supports_signatures must be True if this log formatter supports GPG
1333
1403
Plugins can register functions to show custom revision properties using
1334
1404
the properties_handler_registry. The registered function
1335
must respect the following interface description:
1405
must respect the following interface description::
1336
1407
def my_show_properties(properties_dict):
1337
1408
# code that returns a dict {'name':'value'} of the properties
1342
1413
def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1414
delta_format=None, levels=None, show_advice=False,
1344
to_exact_file=None):
1415
to_exact_file=None, author_list_handler=None):
1345
1416
"""Create a LogFormatter.
1347
1418
:param to_file: the file to output to
1355
1426
let the log formatter decide.
1356
1427
:param show_advice: whether to show advice at the end of the
1429
:param author_list_handler: callable generating a list of
1430
authors to display for a given revision
1359
1432
self.to_file = to_file
1360
1433
# 'exact' stream used to show diff, it should print content 'as is'
1414
1488
def short_author(self, rev):
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1489
return self.authors(rev, 'first', short=True, sep=', ')
1491
def authors(self, rev, who, short=False, sep=None):
1492
"""Generate list of authors, taking --authors option into account.
1494
The caller has to specify the name of a author list handler,
1495
as provided by the author list registry, using the ``who``
1496
argument. That name only sets a default, though: when the
1497
user selected a different author list generation using the
1498
``--authors`` command line switch, as represented by the
1499
``author_list_handler`` constructor argument, that value takes
1502
:param rev: The revision for which to generate the list of authors.
1503
:param who: Name of the default handler.
1504
:param short: Whether to shorten names to either name or address.
1505
:param sep: What separator to use for automatic concatenation.
1507
if self._author_list_handler is not None:
1508
# The user did specify --authors, which overrides the default
1509
author_list_handler = self._author_list_handler
1511
# The user didn't specify --authors, so we use the caller's default
1512
author_list_handler = author_list_registry.get(who)
1513
names = author_list_handler(rev)
1515
for i in range(len(names)):
1516
name, address = config.parse_username(names[i])
1522
names = sep.join(names)
1420
1525
def merge_marker(self, revision):
1421
1526
"""Get the merge marker to include in the output or '' if none."""
1491
1596
supports_delta = True
1492
1597
supports_tags = True
1493
1598
supports_diff = True
1599
supports_signatures = True
1495
1601
def __init__(self, *args, **kwargs):
1496
1602
super(LongLogFormatter, self).__init__(*args, **kwargs)
1516
1622
self.merge_marker(revision)))
1517
1623
if revision.tags:
1518
1624
lines.append('tags: %s' % (', '.join(revision.tags)))
1625
if self.show_ids or revision.revno is None:
1520
1626
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1521
1628
for parent_id in revision.rev.parent_ids:
1522
1629
lines.append('parent: %s' % (parent_id,))
1523
1630
lines.extend(self.custom_properties(revision.rev))
1525
1632
committer = revision.rev.committer
1526
authors = revision.rev.get_apparent_authors()
1633
authors = self.authors(revision.rev, 'all')
1527
1634
if authors != [committer]:
1528
1635
lines.append('author: %s' % (", ".join(authors),))
1529
1636
lines.append('committer: %s' % (committer,))
1535
1642
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1644
if revision.signature is not None:
1645
lines.append('signature: ' + revision.signature)
1537
1647
lines.append('message:')
1538
1648
if not revision.rev.message:
1539
1649
lines.append(' (no message)')
1586
1696
indent = ' ' * depth
1587
1697
revno_width = self.revno_width_by_depth.get(depth)
1588
1698
if revno_width is None:
1589
if revision.revno.find('.') == -1:
1699
if revision.revno is None or revision.revno.find('.') == -1:
1590
1700
# mainline revno, e.g. 12345
1591
1701
revno_width = 5
1600
1710
if revision.tags:
1601
1711
tags = ' {%s}' % (', '.join(revision.tags))
1602
1712
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
revision.revno, self.short_author(revision.rev),
1713
revision.revno or "", self.short_author(revision.rev),
1604
1714
format_date(revision.rev.timestamp,
1605
1715
revision.rev.timezone or 0,
1606
1716
self.show_timezone, date_fmt="%Y-%m-%d",
1607
1717
show_offset=False),
1608
1718
tags, self.merge_marker(revision)))
1609
1719
self.show_properties(revision.rev, indent+offset)
1720
if self.show_ids or revision.revno is None:
1611
1721
to_file.write(indent + offset + 'revision-id:%s\n'
1612
1722
% (revision.rev.revision_id,))
1613
1723
if not revision.rev.message:
1667
1777
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1778
"""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
1780
:param revno: revision number or None.
1781
Revision numbers counts from 1.
1782
:param rev: revision object
1783
:param max_chars: maximum length of resulting string
1784
:param tags: list of tags or None
1785
:param prefix: string to prefix each line
1786
:return: formatted truncated string
1679
1790
# show revno only when is not None
1680
1791
out.append("%s:" % revno)
1681
out.append(self.truncate(self.short_author(rev), 20))
1792
if max_chars is not None:
1793
out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1795
out.append(self.short_author(rev))
1682
1796
out.append(self.date_string(rev))
1683
1797
if len(rev.parent_ids) > 1:
1684
1798
out.append('[merge]')
1703
1817
self.show_timezone,
1704
1818
date_fmt='%Y-%m-%d',
1705
1819
show_offset=False)
1706
committer_str = revision.rev.get_apparent_authors()[0].replace (' <', ' <')
1820
committer_str = self.authors(revision.rev, 'first', sep=', ')
1821
committer_str = committer_str.replace(' <', ' <')
1707
1822
to_file.write('%s %s\n\n' % (date_str,committer_str))
1709
1824
if revision.delta is not None and revision.delta.has_changed():
1774
1889
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1892
def author_list_all(rev):
1893
return rev.get_apparent_authors()[:]
1896
def author_list_first(rev):
1897
lst = rev.get_apparent_authors()
1904
def author_list_committer(rev):
1905
return [rev.committer]
1908
author_list_registry = registry.Registry()
1910
author_list_registry.register('all', author_list_all,
1913
author_list_registry.register('first', author_list_first,
1916
author_list_registry.register('committer', author_list_committer,
1777
1920
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
1921
# deprecated; for compatibility
1779
1922
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1848
1991
old_revisions = set()
1849
1992
new_history = []
1850
1993
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)
1994
graph = repository.get_graph()
1995
new_iter = graph.iter_lefthand_ancestry(new_revision_id)
1996
old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1853
1997
stop_revision = None
1930
2074
lf.log_revision(lr)
1933
def _get_info_for_log_files(revisionspec_list, file_list):
2077
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2078
"""Find file-ids and kinds given a list of files and a revision range.
1936
2080
We search for files at the end of the range. If not found there,
1940
2084
:param file_list: the list of paths given on the command line;
1941
2085
the first of these can be a branch location or a file path,
1942
2086
the remainder must be file paths
2087
:param add_cleanup: When the branch returned is read locked,
2088
an unlock call will be queued to the cleanup.
1943
2089
:return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2090
info_list is a list of (relative_path, file_id, kind) tuples where
1945
2091
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2092
branch will be read-locked.
1948
from builtins import _get_revision_range, safe_relpath_files
2094
from builtins import _get_revision_range
1949
2095
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2096
add_cleanup(b.lock_read().unlock)
1951
2097
# XXX: It's damn messy converting a list of paths to relative paths when
1952
2098
# those paths might be deleted ones, they might be on a case-insensitive
1953
2099
# filesystem and/or they might be in silly locations (like another branch).
1957
2103
# case of running log in a nested directory, assuming paths beyond the
1958
2104
# first one haven't been deleted ...
1960
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2106
relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1962
2108
relpaths = [path] + file_list[1:]