215
214
Logger(branch, rqst).show(lf)
218
# Note: This needs to be kept in sync with the defaults in
217
# Note: This needs to be kept this in sync with the defaults in
219
218
# make_log_request_dict() below
220
219
_DEFAULT_REQUEST_PARAMS = {
221
220
'direction': 'reverse',
223
222
'generate_tags': True,
224
'exclude_common_ancestry': False,
225
223
'_match_using_deltas': True,
229
227
def make_log_request_dict(direction='reverse', specific_fileids=None,
230
start_revision=None, end_revision=None, limit=None,
231
message_search=None, levels=None, generate_tags=True,
233
diff_type=None, _match_using_deltas=True,
234
exclude_common_ancestry=False, match=None,
235
signature=False, omit_merges=False,
228
start_revision=None, end_revision=None, limit=None,
229
message_search=None, levels=1, generate_tags=True, delta_type=None,
230
diff_type=None, _match_using_deltas=True):
237
231
"""Convenience function for making a logging request dictionary.
239
233
Using this function may make code slightly safer by ensuring
278
271
algorithm used for matching specific_fileids. This parameter
279
272
may be removed in the future so bzrlib client code should NOT
282
:param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
283
range operator or as a graph difference.
285
:param signature: show digital signature information
287
:param match: Dictionary of list of search strings to use when filtering
288
revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
289
the empty string to match any of the preceding properties.
291
:param omit_merges: If True, commits with more than one parent are
295
# Take care of old style message_search parameter
298
if 'message' in match:
299
match['message'].append(message_search)
301
match['message'] = [message_search]
303
match={ 'message': [message_search] }
305
276
'direction': direction,
306
277
'specific_fileids': specific_fileids,
307
278
'start_revision': start_revision,
308
279
'end_revision': end_revision,
281
'message_search': message_search,
310
282
'levels': levels,
311
283
'generate_tags': generate_tags,
312
284
'delta_type': delta_type,
313
285
'diff_type': diff_type,
314
'exclude_common_ancestry': exclude_common_ancestry,
315
'signature': signature,
317
'omit_merges': omit_merges,
318
286
# Add 'private' attributes for features that may be deprecated
319
287
'_match_using_deltas': _match_using_deltas,
323
291
def _apply_log_request_defaults(rqst):
324
292
"""Apply default values to a request dictionary."""
325
result = _DEFAULT_REQUEST_PARAMS.copy()
293
result = _DEFAULT_REQUEST_PARAMS
327
295
result.update(rqst)
331
def format_signature_validity(rev_id, repo):
332
"""get the signature validity
334
:param rev_id: revision id to validate
335
:param repo: repository of revision
336
:return: human readable string to print to log
338
from bzrlib import gpg
340
gpg_strategy = gpg.GPGStrategy(None)
341
result = repo.verify_revision(rev_id, gpg_strategy)
342
if result[0] == gpg.SIGNATURE_VALID:
343
return "valid signature from {0}".format(result[1])
344
if result[0] == gpg.SIGNATURE_KEY_MISSING:
345
return "unknown key {0}".format(result[1])
346
if result[0] == gpg.SIGNATURE_NOT_VALID:
347
return "invalid signature!"
348
if result[0] == gpg.SIGNATURE_NOT_SIGNED:
349
return "no signature"
352
299
class LogGenerator(object):
353
300
"""A generator of log revisions."""
399
346
# Tweak the LogRequest based on what the LogFormatter can handle.
400
347
# (There's no point generating stuff if the formatter can't display it.)
402
if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
403
# user didn't specify levels, use whatever the LF can handle:
404
rqst['levels'] = lf.get_levels()
349
rqst['levels'] = lf.get_levels()
406
350
if not getattr(lf, 'supports_tags', False):
407
351
rqst['generate_tags'] = False
408
352
if not getattr(lf, 'supports_delta', False):
409
353
rqst['delta_type'] = None
410
354
if not getattr(lf, 'supports_diff', False):
411
355
rqst['diff_type'] = None
412
if not getattr(lf, 'supports_signatures', False):
413
rqst['signature'] = False
415
357
# Find and print the interesting revisions
416
358
generator = self._generator_factory(self.branch, rqst)
523
455
generate_merge_revisions = rqst.get('levels') != 1
524
456
delayed_graph_generation = not rqst.get('specific_fileids') and (
525
457
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
526
view_revisions = _calc_view_revisions(
527
self.branch, self.start_rev_id, self.end_rev_id,
528
rqst.get('direction'),
529
generate_merge_revisions=generate_merge_revisions,
530
delayed_graph_generation=delayed_graph_generation,
531
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
458
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
459
self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
460
delayed_graph_generation=delayed_graph_generation)
533
462
# Apply the other filters
534
463
return make_log_rev_iterator(self.branch, view_revisions,
535
rqst.get('delta_type'), rqst.get('match'),
464
rqst.get('delta_type'), rqst.get('message_search'),
536
465
file_ids=rqst.get('specific_fileids'),
537
466
direction=rqst.get('direction'))
541
470
# Note that we always generate the merge revisions because
542
471
# filter_revisions_touching_file_id() requires them ...
544
view_revisions = _calc_view_revisions(
545
self.branch, self.start_rev_id, self.end_rev_id,
546
rqst.get('direction'), generate_merge_revisions=True,
547
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
473
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
474
self.end_rev_id, rqst.get('direction'), True)
548
475
if not isinstance(view_revisions, list):
549
476
view_revisions = list(view_revisions)
550
477
view_revisions = _filter_revisions_touching_file_id(self.branch,
551
478
rqst.get('specific_fileids')[0], view_revisions,
552
479
include_merges=rqst.get('levels') != 1)
553
480
return make_log_rev_iterator(self.branch, view_revisions,
554
rqst.get('delta_type'), rqst.get('match'))
481
rqst.get('delta_type'), rqst.get('message_search'))
557
484
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
558
generate_merge_revisions,
559
delayed_graph_generation=False,
560
exclude_common_ancestry=False,
485
generate_merge_revisions, delayed_graph_generation=False):
562
486
"""Calculate the revisions to view.
564
488
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
565
489
a list of the same tuples.
567
if (exclude_common_ancestry and start_rev_id == end_rev_id):
568
raise errors.BzrCommandError(gettext(
569
'--exclude-common-ancestry requires two different revisions'))
570
if direction not in ('reverse', 'forward'):
571
raise ValueError(gettext('invalid direction %r') % direction)
572
491
br_revno, br_rev_id = branch.last_revision_info()
573
492
if br_revno == 0:
576
if (end_rev_id and start_rev_id == end_rev_id
577
and (not generate_merge_revisions
578
or not _has_merges(branch, end_rev_id))):
579
# If a single revision is requested, check we can handle it
580
iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
582
elif not generate_merge_revisions:
583
# If we only want to see linear revisions, we can iterate ...
584
iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
585
direction, exclude_common_ancestry)
586
if direction == 'forward':
587
iter_revs = reversed(iter_revs)
495
# If a single revision is requested, check we can handle it
496
generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
497
(not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
498
if generate_single_revision:
499
return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
501
# If we only want to see linear revisions, we can iterate ...
502
if not generate_merge_revisions:
503
return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
589
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
590
direction, delayed_graph_generation,
591
exclude_common_ancestry)
592
if direction == 'forward':
593
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
506
return _generate_all_revisions(branch, start_rev_id, end_rev_id,
507
direction, delayed_graph_generation)
597
510
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
600
513
return [(br_rev_id, br_revno, 0)]
602
revno_str = _compute_revno_str(branch, rev_id)
515
revno = branch.revision_id_to_dotted_revno(rev_id)
516
revno_str = '.'.join(str(n) for n in revno)
603
517
return [(rev_id, revno_str, 0)]
606
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
607
exclude_common_ancestry=False):
608
result = _linear_view_revisions(
609
branch, start_rev_id, end_rev_id,
610
exclude_common_ancestry=exclude_common_ancestry)
520
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
521
result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
611
522
# If a start limit was given and it's not obviously an
612
523
# ancestor of the end limit, check it before outputting anything
613
524
if direction == 'forward' or (start_rev_id
616
527
result = list(result)
617
528
except _StartNotLinearAncestor:
618
raise errors.BzrCommandError(gettext('Start revision not found in'
619
' left-hand history of end revision.'))
529
raise errors.BzrCommandError('Start revision not found in'
530
' left-hand history of end revision.')
531
if direction == 'forward':
532
result = reversed(result)
623
536
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
624
delayed_graph_generation,
625
exclude_common_ancestry=False):
537
delayed_graph_generation):
626
538
# On large trees, generating the merge graph can take 30-60 seconds
627
539
# so we delay doing it until a merge is detected, incrementally
628
540
# returning initial (non-merge) revisions while we can.
634
546
if delayed_graph_generation:
636
548
for rev_id, revno, depth in _linear_view_revisions(
637
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
549
branch, start_rev_id, end_rev_id):
638
550
if _has_merges(branch, rev_id):
639
551
# The end_rev_id can be nested down somewhere. We need an
640
552
# explicit ancestry check. There is an ambiguity here as we
641
553
# may not raise _StartNotLinearAncestor for a revision that
642
554
# is an ancestor but not a *linear* one. But since we have
643
555
# loaded the graph to do the check (or calculate a dotted
644
# revno), we may as well accept to show the log... We need
645
# the check only if start_rev_id is not None as all
646
# revisions have _mod_revision.NULL_REVISION as an ancestor
556
# revno), we may as well accept to show the log...
648
558
graph = branch.repository.get_graph()
649
if (start_rev_id is not None
650
and not graph.is_ancestor(start_rev_id, end_rev_id)):
559
if not graph.is_ancestor(start_rev_id, end_rev_id):
651
560
raise _StartNotLinearAncestor()
652
# Since we collected the revisions so far, we need to
654
561
end_rev_id = rev_id
657
564
initial_revisions.append((rev_id, revno, depth))
659
566
# No merged revisions found
660
return initial_revisions
567
if direction == 'reverse':
568
return initial_revisions
569
elif direction == 'forward':
570
return reversed(initial_revisions)
572
raise ValueError('invalid direction %r' % direction)
661
573
except _StartNotLinearAncestor:
662
574
# A merge was never detected so the lower revision limit can't
663
575
# be nested down somewhere
664
raise errors.BzrCommandError(gettext('Start revision not found in'
665
' history of end revision.'))
667
# We exit the loop above because we encounter a revision with merges, from
668
# this revision, we need to switch to _graph_view_revisions.
576
raise errors.BzrCommandError('Start revision not found in'
577
' history of end revision.')
670
579
# A log including nested merges is required. If the direction is reverse,
671
580
# we rebase the initial merge depths so that the development line is
674
583
# indented at the end seems slightly nicer in that case.
675
584
view_revisions = chain(iter(initial_revisions),
676
585
_graph_view_revisions(branch, start_rev_id, end_rev_id,
677
rebase_initial_depths=(direction == 'reverse'),
678
exclude_common_ancestry=exclude_common_ancestry))
679
return view_revisions
586
rebase_initial_depths=direction == 'reverse'))
587
if direction == 'reverse':
588
return view_revisions
589
elif direction == 'forward':
590
# Forward means oldest first, adjusting for depth.
591
view_revisions = reverse_by_depth(list(view_revisions))
592
return _rebase_merge_depth(view_revisions)
594
raise ValueError('invalid direction %r' % direction)
682
597
def _has_merges(branch, rev_id):
685
600
return len(parents) > 1
688
def _compute_revno_str(branch, rev_id):
689
"""Compute the revno string from a rev_id.
691
:return: The revno string, or None if the revision is not in the supplied
695
revno = branch.revision_id_to_dotted_revno(rev_id)
696
except errors.NoSuchRevision:
697
# The revision must be outside of this branch
700
return '.'.join(str(n) for n in revno)
703
603
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
704
604
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
705
605
if start_rev_id and end_rev_id:
707
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
708
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
709
except errors.NoSuchRevision:
710
# one or both is not in the branch; not obvious
606
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
607
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
712
608
if len(start_dotted) == 1 and len(end_dotted) == 1:
713
609
# both on mainline
714
610
return start_dotted[0] <= end_dotted[0]
727
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
728
exclude_common_ancestry=False):
623
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
729
624
"""Calculate a sequence of revisions to view, newest to oldest.
731
626
:param start_rev_id: the lower revision-id
732
627
:param end_rev_id: the upper revision-id
733
:param exclude_common_ancestry: Whether the start_rev_id should be part of
734
the iterated revisions.
735
628
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
736
629
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
737
is not found walking the left-hand history
630
is not found walking the left-hand history
739
632
br_revno, br_rev_id = branch.last_revision_info()
740
633
repo = branch.repository
741
graph = repo.get_graph()
742
634
if start_rev_id is None and end_rev_id is None:
743
635
cur_revno = br_revno
744
for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
745
(_mod_revision.NULL_REVISION,)):
636
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
746
637
yield revision_id, str(cur_revno), 0
749
640
if end_rev_id is None:
750
641
end_rev_id = br_rev_id
751
642
found_start = start_rev_id is None
752
for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
753
(_mod_revision.NULL_REVISION,)):
754
revno_str = _compute_revno_str(branch, revision_id)
643
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
644
revno = branch.revision_id_to_dotted_revno(revision_id)
645
revno_str = '.'.join(str(n) for n in revno)
755
646
if not found_start and revision_id == start_rev_id:
756
if not exclude_common_ancestry:
757
yield revision_id, revno_str, 0
647
yield revision_id, revno_str, 0
758
648
found_start = True
808
693
yield rev_id, '.'.join(map(str, revno)), merge_depth
696
@deprecated_function(deprecated_in((2, 2, 0)))
697
def calculate_view_revisions(branch, start_revision, end_revision, direction,
698
specific_fileid, generate_merge_revisions):
699
"""Calculate the revisions to view.
701
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
702
a list of the same tuples.
704
start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
706
view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
707
direction, generate_merge_revisions or specific_fileid))
709
view_revisions = _filter_revisions_touching_file_id(branch,
710
specific_fileid, view_revisions,
711
include_merges=generate_merge_revisions)
712
return _rebase_merge_depth(view_revisions)
811
715
def _rebase_merge_depth(view_revisions):
812
716
"""Adjust depths upwards so the top level is 0."""
813
717
# If either the first or last revision have a merge_depth of 0, we're done
857
761
return log_rev_iterator
860
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
764
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
861
765
"""Create a filtered iterator of log_rev_iterator matching on a regex.
863
767
:param branch: The branch being logged.
864
768
:param generate_delta: Whether to generate a delta for each revision.
865
:param match: A dictionary with properties as keys and lists of strings
866
as values. To match, a revision may match any of the supplied strings
867
within a single property but must match at least one string for each
769
:param search: A user text search string.
869
770
:param log_rev_iterator: An input iterator containing all revisions that
870
771
could be displayed, in lists.
871
772
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
875
776
return log_rev_iterator
876
searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
877
for (k,v) in match.iteritems()]
878
return _filter_re(searchRE, log_rev_iterator)
881
def _filter_re(searchRE, log_rev_iterator):
777
searchRE = re_compile_checked(search, re.IGNORECASE,
778
'log message filter')
779
return _filter_message_re(searchRE, log_rev_iterator)
782
def _filter_message_re(searchRE, log_rev_iterator):
882
783
for revs in log_rev_iterator:
883
new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
887
def _match_filter(searchRE, rev):
889
'message': (rev.message,),
890
'committer': (rev.committer,),
891
'author': (rev.get_apparent_authors()),
892
'bugs': list(rev.iter_bugs())
894
strings[''] = [item for inner_list in strings.itervalues()
895
for item in inner_list]
896
for (k,v) in searchRE:
897
if k in strings and not _match_any_filter(strings[k], v):
901
def _match_any_filter(strings, res):
902
return any([filter(None, map(re.search, strings)) for re in res])
785
for (rev_id, revno, merge_depth), rev, delta in revs:
786
if searchRE.search(rev.message):
787
new_revs.append(((rev_id, revno, merge_depth), rev, delta))
904
791
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
905
792
fileids=None, direction='reverse'):
1138
1026
if ((start_rev_id == _mod_revision.NULL_REVISION)
1139
1027
or (end_rev_id == _mod_revision.NULL_REVISION)):
1140
raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1028
raise errors.BzrCommandError('Logging revision 0 is invalid.')
1141
1029
if start_revno > end_revno:
1142
raise errors.BzrCommandError(gettext("Start revision must be older "
1143
"than the end revision."))
1030
raise errors.BzrCommandError("Start revision must be older than "
1031
"the end revision.")
1145
1033
if end_revno < start_revno:
1146
1034
return None, None, None, None
1147
1035
cur_revno = branch_revno
1149
1037
mainline_revs = []
1150
graph = branch.repository.get_graph()
1151
for revision_id in graph.iter_lefthand_ancestry(
1152
branch_last_revision, (_mod_revision.NULL_REVISION,)):
1038
for revision_id in branch.repository.iter_reverse_revision_history(
1039
branch_last_revision):
1153
1040
if cur_revno < start_revno:
1154
1041
# We have gone far enough, but we always add 1 more revision
1155
1042
rev_nos[revision_id] = cur_revno
1169
1056
return mainline_revs, rev_nos, start_rev_id, end_rev_id
1059
@deprecated_function(deprecated_in((2, 2, 0)))
1060
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1061
"""Filter view_revisions based on revision ranges.
1063
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1064
tuples to be filtered.
1066
:param start_rev_id: If not NONE specifies the first revision to be logged.
1067
If NONE then all revisions up to the end_rev_id are logged.
1069
:param end_rev_id: If not NONE specifies the last revision to be logged.
1070
If NONE then all revisions up to the end of the log are logged.
1072
:return: The filtered view_revisions.
1074
if start_rev_id or end_rev_id:
1075
revision_ids = [r for r, n, d in view_revisions]
1077
start_index = revision_ids.index(start_rev_id)
1080
if start_rev_id == end_rev_id:
1081
end_index = start_index
1084
end_index = revision_ids.index(end_rev_id)
1086
end_index = len(view_revisions) - 1
1087
# To include the revisions merged into the last revision,
1088
# extend end_rev_id down to, but not including, the next rev
1089
# with the same or lesser merge_depth
1090
end_merge_depth = view_revisions[end_index][2]
1092
for index in xrange(end_index+1, len(view_revisions)+1):
1093
if view_revisions[index][2] <= end_merge_depth:
1094
end_index = index - 1
1097
# if the search falls off the end then log to the end as well
1098
end_index = len(view_revisions) - 1
1099
view_revisions = view_revisions[start_index:end_index+1]
1100
return view_revisions
1172
1103
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1173
1104
include_merges=True):
1174
1105
r"""Return the list of revision ids which touch a given file id.
1185
@deprecated_function(deprecated_in((2, 2, 0)))
1186
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1187
include_merges=True):
1188
"""Produce an iterator of revisions to show
1189
:return: an iterator of (revision_id, revno, merge_depth)
1190
(if there is no revno for a revision, None is supplied)
1192
if not include_merges:
1193
revision_ids = mainline_revs[1:]
1194
if direction == 'reverse':
1195
revision_ids.reverse()
1196
for revision_id in revision_ids:
1197
yield revision_id, str(rev_nos[revision_id]), 0
1199
graph = branch.repository.get_graph()
1200
# This asks for all mainline revisions, which means we only have to spider
1201
# sideways, rather than depth history. That said, its still size-of-history
1202
# and should be addressed.
1203
# mainline_revisions always includes an extra revision at the beginning, so
1205
parent_map = dict(((key, value) for key, value in
1206
graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1207
# filter out ghosts; merge_sort errors on ghosts.
1208
rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1209
merge_sorted_revisions = tsort.merge_sort(
1213
generate_revno=True)
1215
if direction == 'forward':
1216
# forward means oldest first.
1217
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1218
elif direction != 'reverse':
1219
raise ValueError('invalid direction %r' % direction)
1221
for (sequence, rev_id, merge_depth, revno, end_of_merge
1222
) in merge_sorted_revisions:
1223
yield rev_id, '.'.join(map(str, revno)), merge_depth
1256
1226
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1257
1227
"""Reverse revisions by depth.
1318
1284
to indicate which LogRevision attributes it supports:
1320
1286
- supports_delta must be True if this log formatter supports delta.
1321
Otherwise the delta attribute may not be populated. The 'delta_format'
1322
attribute describes whether the 'short_status' format (1) or the long
1323
one (2) should be used.
1287
Otherwise the delta attribute may not be populated. The 'delta_format'
1288
attribute describes whether the 'short_status' format (1) or the long
1289
one (2) should be used.
1325
1291
- supports_merge_revisions must be True if this log formatter supports
1326
merge revisions. If not, then only mainline revisions will be passed
1292
merge revisions. If not, then only mainline revisions will be passed
1329
1295
- preferred_levels is the number of levels this formatter defaults to.
1330
The default value is zero meaning display all levels.
1331
This value is only relevant if supports_merge_revisions is True.
1296
The default value is zero meaning display all levels.
1297
This value is only relevant if supports_merge_revisions is True.
1333
1299
- supports_tags must be True if this log formatter supports tags.
1334
Otherwise the tags attribute may not be populated.
1300
Otherwise the tags attribute may not be populated.
1336
1302
- supports_diff must be True if this log formatter supports diffs.
1337
Otherwise the diff attribute may not be populated.
1339
- supports_signatures must be True if this log formatter supports GPG
1303
Otherwise the diff attribute may not be populated.
1342
1305
Plugins can register functions to show custom revision properties using
1343
1306
the properties_handler_registry. The registered function
1344
must respect the following interface description::
1307
must respect the following interface description:
1346
1308
def my_show_properties(properties_dict):
1347
1309
# code that returns a dict {'name':'value'} of the properties
1352
1314
def __init__(self, to_file, show_ids=False, show_timezone='original',
1353
1315
delta_format=None, levels=None, show_advice=False,
1354
to_exact_file=None, author_list_handler=None):
1316
to_exact_file=None):
1355
1317
"""Create a LogFormatter.
1357
1319
:param to_file: the file to output to
1358
:param to_exact_file: if set, gives an output stream to which
1320
:param to_exact_file: if set, gives an output stream to which
1359
1321
non-Unicode diffs are written.
1360
1322
:param show_ids: if True, revision-ids are to be displayed
1361
1323
:param show_timezone: the timezone to use
1427
1386
def short_author(self, rev):
1428
return self.authors(rev, 'first', short=True, sep=', ')
1430
def authors(self, rev, who, short=False, sep=None):
1431
"""Generate list of authors, taking --authors option into account.
1433
The caller has to specify the name of a author list handler,
1434
as provided by the author list registry, using the ``who``
1435
argument. That name only sets a default, though: when the
1436
user selected a different author list generation using the
1437
``--authors`` command line switch, as represented by the
1438
``author_list_handler`` constructor argument, that value takes
1441
:param rev: The revision for which to generate the list of authors.
1442
:param who: Name of the default handler.
1443
:param short: Whether to shorten names to either name or address.
1444
:param sep: What separator to use for automatic concatenation.
1446
if self._author_list_handler is not None:
1447
# The user did specify --authors, which overrides the default
1448
author_list_handler = self._author_list_handler
1450
# The user didn't specify --authors, so we use the caller's default
1451
author_list_handler = author_list_registry.get(who)
1452
names = author_list_handler(rev)
1454
for i in range(len(names)):
1455
name, address = config.parse_username(names[i])
1461
names = sep.join(names)
1387
name, address = config.parse_username(rev.get_apparent_authors()[0])
1464
1392
def merge_marker(self, revision):
1465
1393
"""Get the merge marker to include in the output or '' if none."""
1561
1487
self.merge_marker(revision)))
1562
1488
if revision.tags:
1563
1489
lines.append('tags: %s' % (', '.join(revision.tags)))
1564
if self.show_ids or revision.revno is None:
1565
1491
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1567
1492
for parent_id in revision.rev.parent_ids:
1568
1493
lines.append('parent: %s' % (parent_id,))
1569
1494
lines.extend(self.custom_properties(revision.rev))
1571
1496
committer = revision.rev.committer
1572
authors = self.authors(revision.rev, 'all')
1497
authors = revision.rev.get_apparent_authors()
1573
1498
if authors != [committer]:
1574
1499
lines.append('author: %s' % (", ".join(authors),))
1575
1500
lines.append('committer: %s' % (committer,))
1595
1517
to_file = self.to_file
1596
1518
to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1597
1519
if revision.delta is not None:
1598
# Use the standard status output to display changes
1599
from bzrlib.delta import report_delta
1600
report_delta(to_file, revision.delta, short_status=False,
1601
show_ids=self.show_ids, indent=indent)
1520
# We don't respect delta_format for compatibility
1521
revision.delta.show(to_file, self.show_ids, indent=indent,
1602
1523
if revision.diff is not None:
1603
1524
to_file.write(indent + 'diff:\n')
1604
1525
to_file.flush()
1649
1570
if revision.tags:
1650
1571
tags = ' {%s}' % (', '.join(revision.tags))
1651
1572
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1652
revision.revno or "", self.short_author(revision.rev),
1573
revision.revno, self.short_author(revision.rev),
1653
1574
format_date(revision.rev.timestamp,
1654
1575
revision.rev.timezone or 0,
1655
1576
self.show_timezone, date_fmt="%Y-%m-%d",
1656
1577
show_offset=False),
1657
1578
tags, self.merge_marker(revision)))
1658
1579
self.show_properties(revision.rev, indent+offset)
1659
if self.show_ids or revision.revno is None:
1660
1581
to_file.write(indent + offset + 'revision-id:%s\n'
1661
1582
% (revision.rev.revision_id,))
1662
1583
if not revision.rev.message:
1667
1588
to_file.write(indent + offset + '%s\n' % (l,))
1669
1590
if revision.delta is not None:
1670
# Use the standard status output to display changes
1671
from bzrlib.delta import report_delta
1672
report_delta(to_file, revision.delta,
1673
short_status=self.delta_format==1,
1674
show_ids=self.show_ids, indent=indent + offset)
1591
revision.delta.show(to_file, self.show_ids, indent=indent + offset,
1592
short_status=self.delta_format==1)
1675
1593
if revision.diff is not None:
1676
1594
self.show_diff(self.to_exact_file, revision.diff, ' ')
1677
1595
to_file.write('\n')
1716
1634
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1717
1635
"""Format log info into one string. Truncate tail of string
1719
:param revno: revision number or None.
1720
Revision numbers counts from 1.
1721
:param rev: revision object
1722
:param max_chars: maximum length of resulting string
1723
:param tags: list of tags or None
1724
:param prefix: string to prefix each line
1725
:return: formatted truncated string
1636
:param revno: revision number or None.
1637
Revision numbers counts from 1.
1638
:param rev: revision object
1639
:param max_chars: maximum length of resulting string
1640
:param tags: list of tags or None
1641
:param prefix: string to prefix each line
1642
:return: formatted truncated string
1729
1646
# show revno only when is not None
1730
1647
out.append("%s:" % revno)
1731
if max_chars is not None:
1732
out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1734
out.append(self.short_author(rev))
1648
out.append(self.truncate(self.short_author(rev), 20))
1735
1649
out.append(self.date_string(rev))
1736
1650
if len(rev.parent_ids) > 1:
1737
1651
out.append('[merge]')
1826
1739
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1827
1740
except KeyError:
1828
raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1831
def author_list_all(rev):
1832
return rev.get_apparent_authors()[:]
1835
def author_list_first(rev):
1836
lst = rev.get_apparent_authors()
1843
def author_list_committer(rev):
1844
return [rev.committer]
1847
author_list_registry = registry.Registry()
1849
author_list_registry.register('all', author_list_all,
1852
author_list_registry.register('first', author_list_first,
1855
author_list_registry.register('committer', author_list_committer,
1741
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1744
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1745
# deprecated; for compatibility
1746
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1747
lf.show(revno, rev, delta)
1859
1750
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
2017
1907
:param file_list: the list of paths given on the command line;
2018
1908
the first of these can be a branch location or a file path,
2019
1909
the remainder must be file paths
2020
:param add_cleanup: When the branch returned is read locked,
2021
an unlock call will be queued to the cleanup.
2022
1910
:return: (branch, info_list, start_rev_info, end_rev_info) where
2023
1911
info_list is a list of (relative_path, file_id, kind) tuples where
2024
1912
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2025
1913
branch will be read-locked.
2027
from builtins import _get_revision_range
1915
from builtins import _get_revision_range, safe_relpath_files
2028
1916
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2029
add_cleanup(b.lock_read().unlock)
2030
1918
# XXX: It's damn messy converting a list of paths to relative paths when
2031
1919
# those paths might be deleted ones, they might be on a case-insensitive
2032
1920
# filesystem and/or they might be in silly locations (like another branch).