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