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
74
repository as _mod_repository,
74
75
revision as _mod_revision,
81
82
from bzrlib import (
85
85
from bzrlib.osutils import (
87
87
format_date_with_offset_in_original_timezone,
88
get_diff_header_encoding,
89
88
get_terminal_encoding,
92
92
from bzrlib.symbol_versioning import (
112
112
for revision_id in branch.revision_history():
113
113
this_inv = branch.repository.get_inventory(revision_id)
114
if this_inv.has_id(file_id):
114
if file_id in this_inv:
115
115
this_ie = this_inv[file_id]
116
116
this_path = this_inv.id2path(file_id)
233
233
diff_type=None, _match_using_deltas=True,
234
234
exclude_common_ancestry=False,
237
236
"""Convenience function for making a logging request dictionary.
262
261
generate; 1 for just the mainline; 0 for all levels.
264
263
:param generate_tags: If True, include tags for matched revisions.
266
265
:param delta_type: Either 'full', 'partial' or None.
267
266
'full' means generate the complete delta - adds/deletes/modifies/etc;
268
267
'partial' means filter the delta using specific_fileids;
295
292
'delta_type': delta_type,
296
293
'diff_type': diff_type,
297
294
'exclude_common_ancestry': exclude_common_ancestry,
298
'signature': signature,
299
295
# Add 'private' attributes for features that may be deprecated
300
296
'_match_using_deltas': _match_using_deltas,
304
300
def _apply_log_request_defaults(rqst):
305
301
"""Apply default values to a request dictionary."""
306
result = _DEFAULT_REQUEST_PARAMS.copy()
302
result = _DEFAULT_REQUEST_PARAMS
308
304
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
308
class LogGenerator(object):
334
309
"""A generator of log revisions."""
387
362
rqst['delta_type'] = None
388
363
if not getattr(lf, 'supports_diff', False):
389
364
rqst['diff_type'] = None
390
if not getattr(lf, 'supports_signatures', False):
391
rqst['signature'] = False
393
366
# Find and print the interesting revisions
394
367
generator = self._generator_factory(self.branch, rqst)
428
401
levels = rqst.get('levels')
429
402
limit = rqst.get('limit')
430
403
diff_type = rqst.get('diff_type')
431
show_signature = rqst.get('signature')
433
405
revision_iterator = self._create_log_revision_iterator()
434
406
for revs in revision_iterator:
442
414
diff = self._format_diff(rev, rev_id, diff_type)
444
signature = format_signature_validity(rev_id,
445
self.branch.repository)
448
415
yield LogRevision(rev, revno, merge_depth, delta,
449
self.rev_tag_dict.get(rev_id), diff, signature)
416
self.rev_tag_dict.get(rev_id), diff)
452
419
if log_count >= limit:
467
434
specific_files = None
469
path_encoding = get_diff_header_encoding()
436
path_encoding = osutils.get_diff_header_encoding()
470
437
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
471
438
new_label='', path_encoding=path_encoding)
472
439
return s.getvalue()
575
542
return [(br_rev_id, br_revno, 0)]
577
revno_str = _compute_revno_str(branch, rev_id)
544
revno = branch.revision_id_to_dotted_revno(rev_id)
545
revno_str = '.'.join(str(n) for n in revno)
578
546
return [(rev_id, revno_str, 0)]
660
628
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
631
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
679
632
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
680
633
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
634
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
635
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
687
636
if len(start_dotted) == 1 and len(end_dotted) == 1:
688
637
# both on mainline
689
638
return start_dotted[0] <= end_dotted[0]
714
663
br_revno, br_rev_id = branch.last_revision_info()
715
664
repo = branch.repository
716
graph = repo.get_graph()
717
665
if start_rev_id is None and end_rev_id is None:
718
666
cur_revno = br_revno
719
for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
720
(_mod_revision.NULL_REVISION,)):
667
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
721
668
yield revision_id, str(cur_revno), 0
724
671
if end_rev_id is None:
725
672
end_rev_id = br_rev_id
726
673
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)
674
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
675
revno = branch.revision_id_to_dotted_revno(revision_id)
676
revno_str = '.'.join(str(n) for n in revno)
730
677
if not found_start and revision_id == start_rev_id:
731
678
if not exclude_common_ancestry:
732
679
yield revision_id, revno_str, 0
865
812
if search is None:
866
813
return log_rev_iterator
867
searchRE = lazy_regex.lazy_compile(search, re.IGNORECASE)
814
searchRE = re_compile_checked(search, re.IGNORECASE,
815
'log message filter')
868
816
return _filter_message_re(searchRE, log_rev_iterator)
1124
1072
cur_revno = branch_revno
1126
1074
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,)):
1075
for revision_id in branch.repository.iter_reverse_revision_history(
1076
branch_last_revision):
1130
1077
if cur_revno < start_revno:
1131
1078
# We have gone far enough, but we always add 1 more revision
1132
1079
rev_nos[revision_id] = cur_revno
1232
1178
# 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
1180
text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1237
1181
next_keys = None
1238
1182
# Looking up keys in batches of 1000 can cut the time in half, as well as
1242
1186
# indexing layer. We might consider passing in hints as to the known
1243
1187
# access pattern (sparse/clustered, high success rate/low success
1244
1188
# rate). This particular access is clustered with a low success rate.
1189
get_parent_map = branch.repository.texts.get_parent_map
1245
1190
modified_text_revisions = set()
1246
1191
chunk_size = 1000
1247
1192
for start in xrange(0, len(text_keys), chunk_size):
1357
1302
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1358
tags=None, diff=None, signature=None):
1303
tags=None, diff=None):
1363
self.revno = str(revno)
1305
self.revno = str(revno)
1364
1306
self.merge_depth = merge_depth
1365
1307
self.delta = delta
1366
1308
self.tags = tags
1367
1309
self.diff = diff
1368
self.signature = signature
1371
1312
class LogFormatter(object):
1380
1321
to indicate which LogRevision attributes it supports:
1382
1323
- 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.
1324
Otherwise the delta attribute may not be populated. The 'delta_format'
1325
attribute describes whether the 'short_status' format (1) or the long
1326
one (2) should be used.
1387
1328
- supports_merge_revisions must be True if this log formatter supports
1388
merge revisions. If not, then only mainline revisions will be passed
1329
merge revisions. If not, then only mainline revisions will be passed
1391
1332
- 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.
1333
The default value is zero meaning display all levels.
1334
This value is only relevant if supports_merge_revisions is True.
1395
1336
- supports_tags must be True if this log formatter supports tags.
1396
Otherwise the tags attribute may not be populated.
1337
Otherwise the tags attribute may not be populated.
1398
1339
- 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
1340
Otherwise the diff attribute may not be populated.
1404
1342
Plugins can register functions to show custom revision properties using
1405
1343
the properties_handler_registry. The registered function
1406
must respect the following interface description::
1344
must respect the following interface description:
1408
1345
def my_show_properties(properties_dict):
1409
1346
# code that returns a dict {'name':'value'} of the properties
1597
1534
supports_delta = True
1598
1535
supports_tags = True
1599
1536
supports_diff = True
1600
supports_signatures = True
1602
1538
def __init__(self, *args, **kwargs):
1603
1539
super(LongLogFormatter, self).__init__(*args, **kwargs)
1623
1559
self.merge_marker(revision)))
1624
1560
if revision.tags:
1625
1561
lines.append('tags: %s' % (', '.join(revision.tags)))
1626
if self.show_ids or revision.revno is None:
1627
1563
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1629
1564
for parent_id in revision.rev.parent_ids:
1630
1565
lines.append('parent: %s' % (parent_id,))
1631
1566
lines.extend(self.custom_properties(revision.rev))
1643
1578
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1645
if revision.signature is not None:
1646
lines.append('signature: ' + revision.signature)
1648
1580
lines.append('message:')
1649
1581
if not revision.rev.message:
1650
1582
lines.append(' (no message)')
1697
1629
indent = ' ' * depth
1698
1630
revno_width = self.revno_width_by_depth.get(depth)
1699
1631
if revno_width is None:
1700
if revision.revno is None or revision.revno.find('.') == -1:
1632
if revision.revno.find('.') == -1:
1701
1633
# mainline revno, e.g. 12345
1702
1634
revno_width = 5
1711
1643
if revision.tags:
1712
1644
tags = ' {%s}' % (', '.join(revision.tags))
1713
1645
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1714
revision.revno or "", self.short_author(revision.rev),
1646
revision.revno, self.short_author(revision.rev),
1715
1647
format_date(revision.rev.timestamp,
1716
1648
revision.rev.timezone or 0,
1717
1649
self.show_timezone, date_fmt="%Y-%m-%d",
1718
1650
show_offset=False),
1719
1651
tags, self.merge_marker(revision)))
1720
1652
self.show_properties(revision.rev, indent+offset)
1721
if self.show_ids or revision.revno is None:
1722
1654
to_file.write(indent + offset + 'revision-id:%s\n'
1723
1655
% (revision.rev.revision_id,))
1724
1656
if not revision.rev.message:
1778
1710
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1779
1711
"""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
1712
:param revno: revision number or None.
1713
Revision numbers counts from 1.
1714
:param rev: revision object
1715
:param max_chars: maximum length of resulting string
1716
:param tags: list of tags or None
1717
:param prefix: string to prefix each line
1718
:return: formatted truncated string
1791
1722
# show revno only when is not None
1792
1723
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))
1724
out.append(self.truncate(self.short_author(rev), 20))
1797
1725
out.append(self.date_string(rev))
1798
1726
if len(rev.parent_ids) > 1:
1799
1727
out.append('[merge]')
1992
1920
old_revisions = set()
1993
1921
new_history = []
1994
1922
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)
1923
new_iter = repository.iter_reverse_revision_history(new_revision_id)
1924
old_iter = repository.iter_reverse_revision_history(old_revision_id)
1998
1925
stop_revision = None
2092
2019
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2093
2020
branch will be read-locked.
2095
from builtins import _get_revision_range
2022
from builtins import _get_revision_range, safe_relpath_files
2096
2023
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2097
2024
add_cleanup(b.lock_read().unlock)
2098
2025
# XXX: It's damn messy converting a list of paths to relative paths when
2104
2031
# case of running log in a nested directory, assuming paths beyond the
2105
2032
# first one haven't been deleted ...
2107
relpaths = [path] + tree.safe_relpath_files(file_list[1:])
2034
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2109
2036
relpaths = [path] + file_list[1:]