214
218
Logger(branch, rqst).show(lf)
217
# Note: This needs to be kept this in sync with the defaults in
221
# Note: This needs to be kept in sync with the defaults in
218
222
# make_log_request_dict() below
219
223
_DEFAULT_REQUEST_PARAMS = {
220
224
'direction': 'reverse',
222
226
'generate_tags': True,
227
'exclude_common_ancestry': False,
223
228
'_match_using_deltas': True,
227
232
def make_log_request_dict(direction='reverse', specific_fileids=None,
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):
233
start_revision=None, end_revision=None, limit=None,
234
message_search=None, levels=None, generate_tags=True,
236
diff_type=None, _match_using_deltas=True,
237
exclude_common_ancestry=False, match=None,
238
signature=False, omit_merges=False,
231
240
"""Convenience function for making a logging request dictionary.
233
242
Using this function may make code slightly safer by ensuring
271
281
algorithm used for matching specific_fileids. This parameter
272
282
may be removed in the future so bzrlib client code should NOT
285
:param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
286
range operator or as a graph difference.
288
:param signature: show digital signature information
290
:param match: Dictionary of list of search strings to use when filtering
291
revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
292
the empty string to match any of the preceding properties.
294
:param omit_merges: If True, commits with more than one parent are
298
# Take care of old style message_search parameter
301
if 'message' in match:
302
match['message'].append(message_search)
304
match['message'] = [message_search]
306
match={ 'message': [message_search] }
276
308
'direction': direction,
277
309
'specific_fileids': specific_fileids,
278
310
'start_revision': start_revision,
279
311
'end_revision': end_revision,
281
'message_search': message_search,
282
313
'levels': levels,
283
314
'generate_tags': generate_tags,
284
315
'delta_type': delta_type,
285
316
'diff_type': diff_type,
317
'exclude_common_ancestry': exclude_common_ancestry,
318
'signature': signature,
320
'omit_merges': omit_merges,
286
321
# Add 'private' attributes for features that may be deprecated
287
322
'_match_using_deltas': _match_using_deltas,
291
326
def _apply_log_request_defaults(rqst):
292
327
"""Apply default values to a request dictionary."""
293
result = _DEFAULT_REQUEST_PARAMS
328
result = _DEFAULT_REQUEST_PARAMS.copy()
295
330
result.update(rqst)
334
def format_signature_validity(rev_id, repo):
335
"""get the signature validity
337
:param rev_id: revision id to validate
338
:param repo: repository of revision
339
:return: human readable string to print to log
341
from bzrlib import gpg
343
gpg_strategy = gpg.GPGStrategy(None)
344
result = repo.verify_revision_signature(rev_id, gpg_strategy)
345
if result[0] == gpg.SIGNATURE_VALID:
346
return "valid signature from {0}".format(result[1])
347
if result[0] == gpg.SIGNATURE_KEY_MISSING:
348
return "unknown key {0}".format(result[1])
349
if result[0] == gpg.SIGNATURE_NOT_VALID:
350
return "invalid signature!"
351
if result[0] == gpg.SIGNATURE_NOT_SIGNED:
352
return "no signature"
299
355
class LogGenerator(object):
300
356
"""A generator of log revisions."""
346
402
# Tweak the LogRequest based on what the LogFormatter can handle.
347
403
# (There's no point generating stuff if the formatter can't display it.)
349
rqst['levels'] = lf.get_levels()
405
if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
406
# user didn't specify levels, use whatever the LF can handle:
407
rqst['levels'] = lf.get_levels()
350
409
if not getattr(lf, 'supports_tags', False):
351
410
rqst['generate_tags'] = False
352
411
if not getattr(lf, 'supports_delta', False):
353
412
rqst['delta_type'] = None
354
413
if not getattr(lf, 'supports_diff', False):
355
414
rqst['diff_type'] = None
415
if not getattr(lf, 'supports_signatures', False):
416
rqst['signature'] = False
357
418
# Find and print the interesting revisions
358
419
generator = self._generator_factory(self.branch, rqst)
455
526
generate_merge_revisions = rqst.get('levels') != 1
456
527
delayed_graph_generation = not rqst.get('specific_fileids') and (
457
528
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
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)
529
view_revisions = _calc_view_revisions(
530
self.branch, self.start_rev_id, self.end_rev_id,
531
rqst.get('direction'),
532
generate_merge_revisions=generate_merge_revisions,
533
delayed_graph_generation=delayed_graph_generation,
534
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
462
536
# Apply the other filters
463
537
return make_log_rev_iterator(self.branch, view_revisions,
464
rqst.get('delta_type'), rqst.get('message_search'),
538
rqst.get('delta_type'), rqst.get('match'),
465
539
file_ids=rqst.get('specific_fileids'),
466
540
direction=rqst.get('direction'))
470
544
# Note that we always generate the merge revisions because
471
545
# filter_revisions_touching_file_id() requires them ...
473
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
474
self.end_rev_id, rqst.get('direction'), True)
547
view_revisions = _calc_view_revisions(
548
self.branch, self.start_rev_id, self.end_rev_id,
549
rqst.get('direction'), generate_merge_revisions=True,
550
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
475
551
if not isinstance(view_revisions, list):
476
552
view_revisions = list(view_revisions)
477
553
view_revisions = _filter_revisions_touching_file_id(self.branch,
478
554
rqst.get('specific_fileids')[0], view_revisions,
479
555
include_merges=rqst.get('levels') != 1)
480
556
return make_log_rev_iterator(self.branch, view_revisions,
481
rqst.get('delta_type'), rqst.get('message_search'))
557
rqst.get('delta_type'), rqst.get('match'))
484
560
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
485
generate_merge_revisions, delayed_graph_generation=False):
561
generate_merge_revisions,
562
delayed_graph_generation=False,
563
exclude_common_ancestry=False,
486
565
"""Calculate the revisions to view.
488
567
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
489
568
a list of the same tuples.
570
if (exclude_common_ancestry and start_rev_id == end_rev_id):
571
raise errors.BzrCommandError(gettext(
572
'--exclude-common-ancestry requires two different revisions'))
573
if direction not in ('reverse', 'forward'):
574
raise ValueError(gettext('invalid direction %r') % direction)
491
575
br_revno, br_rev_id = branch.last_revision_info()
492
576
if br_revno == 0:
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 ...
579
if (end_rev_id and start_rev_id == end_rev_id
580
and (not generate_merge_revisions
581
or not _has_merges(branch, end_rev_id))):
582
# If a single revision is requested, check we can handle it
583
return _generate_one_revision(branch, end_rev_id, br_rev_id,
502
585
if not generate_merge_revisions:
503
return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
506
return _generate_all_revisions(branch, start_rev_id, end_rev_id,
507
direction, delayed_graph_generation)
587
# If we only want to see linear revisions, we can iterate ...
588
iter_revs = _linear_view_revisions(
589
branch, start_rev_id, end_rev_id,
590
exclude_common_ancestry=exclude_common_ancestry)
591
# If a start limit was given and it's not obviously an
592
# ancestor of the end limit, check it before outputting anything
593
if (direction == 'forward'
594
or (start_rev_id and not _is_obvious_ancestor(
595
branch, start_rev_id, end_rev_id))):
596
iter_revs = list(iter_revs)
597
if direction == 'forward':
598
iter_revs = reversed(iter_revs)
600
except _StartNotLinearAncestor:
601
# Switch to the slower implementation that may be able to find a
602
# non-obvious ancestor out of the left-hand history.
604
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
605
direction, delayed_graph_generation,
606
exclude_common_ancestry)
607
if direction == 'forward':
608
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
510
612
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
513
615
return [(br_rev_id, br_revno, 0)]
515
revno = branch.revision_id_to_dotted_revno(rev_id)
516
revno_str = '.'.join(str(n) for n in revno)
617
revno_str = _compute_revno_str(branch, rev_id)
517
618
return [(rev_id, revno_str, 0)]
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)
522
# If a start limit was given and it's not obviously an
523
# ancestor of the end limit, check it before outputting anything
524
if direction == 'forward' or (start_rev_id
525
and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
527
result = list(result)
528
except _StartNotLinearAncestor:
529
raise errors.BzrCommandError('Start revision not found in'
530
' left-hand history of end revision.')
531
if direction == 'forward':
532
result = reversed(result)
536
621
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
537
delayed_graph_generation):
622
delayed_graph_generation,
623
exclude_common_ancestry=False):
538
624
# On large trees, generating the merge graph can take 30-60 seconds
539
625
# so we delay doing it until a merge is detected, incrementally
540
626
# returning initial (non-merge) revisions while we can.
546
632
if delayed_graph_generation:
548
634
for rev_id, revno, depth in _linear_view_revisions(
549
branch, start_rev_id, end_rev_id):
635
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
550
636
if _has_merges(branch, rev_id):
551
637
# The end_rev_id can be nested down somewhere. We need an
552
638
# explicit ancestry check. There is an ambiguity here as we
553
639
# may not raise _StartNotLinearAncestor for a revision that
554
640
# is an ancestor but not a *linear* one. But since we have
555
641
# loaded the graph to do the check (or calculate a dotted
556
# revno), we may as well accept to show the log...
642
# revno), we may as well accept to show the log... We need
643
# the check only if start_rev_id is not None as all
644
# revisions have _mod_revision.NULL_REVISION as an ancestor
558
646
graph = branch.repository.get_graph()
559
if not graph.is_ancestor(start_rev_id, end_rev_id):
647
if (start_rev_id is not None
648
and not graph.is_ancestor(start_rev_id, end_rev_id)):
560
649
raise _StartNotLinearAncestor()
650
# Since we collected the revisions so far, we need to
561
652
end_rev_id = rev_id
564
655
initial_revisions.append((rev_id, revno, depth))
566
657
# No merged revisions found
567
if direction == 'reverse':
568
return initial_revisions
569
elif direction == 'forward':
570
return reversed(initial_revisions)
572
raise ValueError('invalid direction %r' % direction)
658
return initial_revisions
573
659
except _StartNotLinearAncestor:
574
660
# A merge was never detected so the lower revision limit can't
575
661
# be nested down somewhere
576
raise errors.BzrCommandError('Start revision not found in'
577
' history of end revision.')
662
raise errors.BzrCommandError(gettext('Start revision not found in'
663
' history of end revision.'))
665
# We exit the loop above because we encounter a revision with merges, from
666
# this revision, we need to switch to _graph_view_revisions.
579
668
# A log including nested merges is required. If the direction is reverse,
580
669
# we rebase the initial merge depths so that the development line is
583
672
# indented at the end seems slightly nicer in that case.
584
673
view_revisions = chain(iter(initial_revisions),
585
674
_graph_view_revisions(branch, start_rev_id, end_rev_id,
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)
675
rebase_initial_depths=(direction == 'reverse'),
676
exclude_common_ancestry=exclude_common_ancestry))
677
return view_revisions
597
680
def _has_merges(branch, rev_id):
600
683
return len(parents) > 1
686
def _compute_revno_str(branch, rev_id):
687
"""Compute the revno string from a rev_id.
689
:return: The revno string, or None if the revision is not in the supplied
693
revno = branch.revision_id_to_dotted_revno(rev_id)
694
except errors.NoSuchRevision:
695
# The revision must be outside of this branch
698
return '.'.join(str(n) for n in revno)
603
701
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
604
702
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
605
703
if start_rev_id and end_rev_id:
606
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
607
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
705
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
706
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
707
except errors.NoSuchRevision:
708
# one or both is not in the branch; not obvious
608
710
if len(start_dotted) == 1 and len(end_dotted) == 1:
609
711
# both on mainline
610
712
return start_dotted[0] <= end_dotted[0]
623
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
725
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
726
exclude_common_ancestry=False):
624
727
"""Calculate a sequence of revisions to view, newest to oldest.
626
729
:param start_rev_id: the lower revision-id
627
730
:param end_rev_id: the upper revision-id
731
:param exclude_common_ancestry: Whether the start_rev_id should be part of
732
the iterated revisions.
628
733
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
629
734
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
630
is not found walking the left-hand history
735
is not found walking the left-hand history
632
737
br_revno, br_rev_id = branch.last_revision_info()
633
738
repo = branch.repository
739
graph = repo.get_graph()
634
740
if start_rev_id is None and end_rev_id is None:
635
741
cur_revno = br_revno
636
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
742
for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
743
(_mod_revision.NULL_REVISION,)):
637
744
yield revision_id, str(cur_revno), 0
640
747
if end_rev_id is None:
641
748
end_rev_id = br_rev_id
642
749
found_start = start_rev_id is None
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)
750
for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
751
(_mod_revision.NULL_REVISION,)):
752
revno_str = _compute_revno_str(branch, revision_id)
646
753
if not found_start and revision_id == start_rev_id:
647
yield revision_id, revno_str, 0
754
if not exclude_common_ancestry:
755
yield revision_id, revno_str, 0
648
756
found_start = True
693
806
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)
715
809
def _rebase_merge_depth(view_revisions):
716
810
"""Adjust depths upwards so the top level is 0."""
717
811
# If either the first or last revision have a merge_depth of 0, we're done
761
855
return log_rev_iterator
764
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
858
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
765
859
"""Create a filtered iterator of log_rev_iterator matching on a regex.
767
861
:param branch: The branch being logged.
768
862
:param generate_delta: Whether to generate a delta for each revision.
769
:param search: A user text search string.
863
:param match: A dictionary with properties as keys and lists of strings
864
as values. To match, a revision may match any of the supplied strings
865
within a single property but must match at least one string for each
770
867
:param log_rev_iterator: An input iterator containing all revisions that
771
868
could be displayed, in lists.
772
869
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
776
873
return 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):
874
searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
875
for (k,v) in match.iteritems()]
876
return _filter_re(searchRE, log_rev_iterator)
879
def _filter_re(searchRE, log_rev_iterator):
783
880
for revs in log_rev_iterator:
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))
881
new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
885
def _match_filter(searchRE, rev):
887
'message': (rev.message,),
888
'committer': (rev.committer,),
889
'author': (rev.get_apparent_authors()),
890
'bugs': list(rev.iter_bugs())
892
strings[''] = [item for inner_list in strings.itervalues()
893
for item in inner_list]
894
for (k,v) in searchRE:
895
if k in strings and not _match_any_filter(strings[k], v):
899
def _match_any_filter(strings, res):
900
return any([filter(None, map(re.search, strings)) for re in res])
791
902
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
792
903
fileids=None, direction='reverse'):
1026
1136
if ((start_rev_id == _mod_revision.NULL_REVISION)
1027
1137
or (end_rev_id == _mod_revision.NULL_REVISION)):
1028
raise errors.BzrCommandError('Logging revision 0 is invalid.')
1138
raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1029
1139
if start_revno > end_revno:
1030
raise errors.BzrCommandError("Start revision must be older than "
1031
"the end revision.")
1140
raise errors.BzrCommandError(gettext("Start revision must be older "
1141
"than the end revision."))
1033
1143
if end_revno < start_revno:
1034
1144
return None, None, None, None
1035
1145
cur_revno = branch_revno
1037
1147
mainline_revs = []
1038
for revision_id in branch.repository.iter_reverse_revision_history(
1039
branch_last_revision):
1148
graph = branch.repository.get_graph()
1149
for revision_id in graph.iter_lefthand_ancestry(
1150
branch_last_revision, (_mod_revision.NULL_REVISION,)):
1040
1151
if cur_revno < start_revno:
1041
1152
# We have gone far enough, but we always add 1 more revision
1042
1153
rev_nos[revision_id] = cur_revno
1056
1167
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
1103
1170
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1104
1171
include_merges=True):
1105
1172
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
1226
1254
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1227
1255
"""Reverse revisions by depth.
1284
1316
to indicate which LogRevision attributes it supports:
1286
1318
- supports_delta must be True if this log formatter supports delta.
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.
1319
Otherwise the delta attribute may not be populated. The 'delta_format'
1320
attribute describes whether the 'short_status' format (1) or the long
1321
one (2) should be used.
1291
1323
- supports_merge_revisions must be True if this log formatter supports
1292
merge revisions. If not, then only mainline revisions will be passed
1324
merge revisions. If not, then only mainline revisions will be passed
1295
1327
- preferred_levels is the number of levels this formatter defaults to.
1296
The default value is zero meaning display all levels.
1297
This value is only relevant if supports_merge_revisions is True.
1328
The default value is zero meaning display all levels.
1329
This value is only relevant if supports_merge_revisions is True.
1299
1331
- supports_tags must be True if this log formatter supports tags.
1300
Otherwise the tags attribute may not be populated.
1332
Otherwise the tags attribute may not be populated.
1302
1334
- supports_diff must be True if this log formatter supports diffs.
1303
Otherwise the diff attribute may not be populated.
1335
Otherwise the diff attribute may not be populated.
1337
- supports_signatures must be True if this log formatter supports GPG
1305
1340
Plugins can register functions to show custom revision properties using
1306
1341
the properties_handler_registry. The registered function
1307
must respect the following interface description:
1342
must respect the following interface description::
1308
1344
def my_show_properties(properties_dict):
1309
1345
# code that returns a dict {'name':'value'} of the properties
1386
1425
def short_author(self, rev):
1387
name, address = config.parse_username(rev.get_apparent_authors()[0])
1426
return self.authors(rev, 'first', short=True, sep=', ')
1428
def authors(self, rev, who, short=False, sep=None):
1429
"""Generate list of authors, taking --authors option into account.
1431
The caller has to specify the name of a author list handler,
1432
as provided by the author list registry, using the ``who``
1433
argument. That name only sets a default, though: when the
1434
user selected a different author list generation using the
1435
``--authors`` command line switch, as represented by the
1436
``author_list_handler`` constructor argument, that value takes
1439
:param rev: The revision for which to generate the list of authors.
1440
:param who: Name of the default handler.
1441
:param short: Whether to shorten names to either name or address.
1442
:param sep: What separator to use for automatic concatenation.
1444
if self._author_list_handler is not None:
1445
# The user did specify --authors, which overrides the default
1446
author_list_handler = self._author_list_handler
1448
# The user didn't specify --authors, so we use the caller's default
1449
author_list_handler = author_list_registry.get(who)
1450
names = author_list_handler(rev)
1452
for i in range(len(names)):
1453
name, address = config.parse_username(names[i])
1459
names = sep.join(names)
1392
1462
def merge_marker(self, revision):
1393
1463
"""Get the merge marker to include in the output or '' if none."""
1487
1559
self.merge_marker(revision)))
1488
1560
if revision.tags:
1489
1561
lines.append('tags: %s' % (', '.join(revision.tags)))
1562
if self.show_ids or revision.revno is None:
1491
1563
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1492
1565
for parent_id in revision.rev.parent_ids:
1493
1566
lines.append('parent: %s' % (parent_id,))
1494
1567
lines.extend(self.custom_properties(revision.rev))
1496
1569
committer = revision.rev.committer
1497
authors = revision.rev.get_apparent_authors()
1570
authors = self.authors(revision.rev, 'all')
1498
1571
if authors != [committer]:
1499
1572
lines.append('author: %s' % (", ".join(authors),))
1500
1573
lines.append('committer: %s' % (committer,))
1570
1647
if revision.tags:
1571
1648
tags = ' {%s}' % (', '.join(revision.tags))
1572
1649
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1573
revision.revno, self.short_author(revision.rev),
1650
revision.revno or "", self.short_author(revision.rev),
1574
1651
format_date(revision.rev.timestamp,
1575
1652
revision.rev.timezone or 0,
1576
1653
self.show_timezone, date_fmt="%Y-%m-%d",
1577
1654
show_offset=False),
1578
1655
tags, self.merge_marker(revision)))
1579
1656
self.show_properties(revision.rev, indent+offset)
1657
if self.show_ids or revision.revno is None:
1581
1658
to_file.write(indent + offset + 'revision-id:%s\n'
1582
1659
% (revision.rev.revision_id,))
1583
1660
if not revision.rev.message:
1634
1714
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1635
1715
"""Format log info into one string. Truncate tail of 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
1717
:param revno: revision number or None.
1718
Revision numbers counts from 1.
1719
:param rev: revision object
1720
:param max_chars: maximum length of resulting string
1721
:param tags: list of tags or None
1722
:param prefix: string to prefix each line
1723
:return: formatted truncated string
1646
1727
# show revno only when is not None
1647
1728
out.append("%s:" % revno)
1648
out.append(self.truncate(self.short_author(rev), 20))
1729
if max_chars is not None:
1730
out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1732
out.append(self.short_author(rev))
1649
1733
out.append(self.date_string(rev))
1650
1734
if len(rev.parent_ids) > 1:
1651
1735
out.append('[merge]')
1709
1794
return self.get(name)(*args, **kwargs)
1711
1796
def get_default(self, branch):
1712
return self.get(branch.get_config().log_format())
1797
c = branch.get_config_stack()
1798
return self.get(c.get('log_format'))
1715
1801
log_formatter_registry = LogFormatterRegistry()
1718
1804
log_formatter_registry.register('short', ShortLogFormatter,
1719
'Moderately short log format')
1805
'Moderately short log format.')
1720
1806
log_formatter_registry.register('long', LongLogFormatter,
1721
'Detailed log format')
1807
'Detailed log format.')
1722
1808
log_formatter_registry.register('line', LineLogFormatter,
1723
'Log format with one line per revision')
1809
'Log format with one line per revision.')
1724
1810
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1725
'Format used by GNU ChangeLog files')
1811
'Format used by GNU ChangeLog files.')
1728
1814
def register_formatter(name, formatter):
1739
1825
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1740
1826
except KeyError:
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)
1827
raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1830
def author_list_all(rev):
1831
return rev.get_apparent_authors()[:]
1834
def author_list_first(rev):
1835
lst = rev.get_apparent_authors()
1842
def author_list_committer(rev):
1843
return [rev.committer]
1846
author_list_registry = registry.Registry()
1848
author_list_registry.register('all', author_list_all,
1851
author_list_registry.register('first', author_list_first,
1854
author_list_registry.register('committer', author_list_committer,
1750
1858
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1907
2016
:param file_list: the list of paths given on the command line;
1908
2017
the first of these can be a branch location or a file path,
1909
2018
the remainder must be file paths
2019
:param add_cleanup: When the branch returned is read locked,
2020
an unlock call will be queued to the cleanup.
1910
2021
:return: (branch, info_list, start_rev_info, end_rev_info) where
1911
2022
info_list is a list of (relative_path, file_id, kind) tuples where
1912
2023
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1913
2024
branch will be read-locked.
1915
from builtins import _get_revision_range, safe_relpath_files
1916
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2026
from bzrlib.builtins import _get_revision_range
2027
tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2029
add_cleanup(b.lock_read().unlock)
1918
2030
# XXX: It's damn messy converting a list of paths to relative paths when
1919
2031
# those paths might be deleted ones, they might be on a case-insensitive
1920
2032
# filesystem and/or they might be in silly locations (like another branch).