219
213
# make_log_request_dict() below
220
214
_DEFAULT_REQUEST_PARAMS = {
221
215
'direction': 'reverse',
223
217
'generate_tags': True,
224
'exclude_common_ancestry': False,
225
218
'_match_using_deltas': True,
229
222
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,
223
start_revision=None, end_revision=None, limit=None,
224
message_search=None, levels=1, generate_tags=True, delta_type=None,
225
diff_type=None, _match_using_deltas=True):
237
226
"""Convenience function for making a logging request dictionary.
239
228
Using this function may make code slightly safer by ensuring
278
266
algorithm used for matching specific_fileids. This parameter
279
267
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.
292
# Take care of old style message_search parameter
295
if 'message' in match:
296
match['message'].append(message_search)
298
match['message'] = [message_search]
300
match={ 'message': [message_search] }
302
271
'direction': direction,
303
272
'specific_fileids': specific_fileids,
304
273
'start_revision': start_revision,
305
274
'end_revision': end_revision,
276
'message_search': message_search,
307
277
'levels': levels,
308
278
'generate_tags': generate_tags,
309
279
'delta_type': delta_type,
310
280
'diff_type': diff_type,
311
'exclude_common_ancestry': exclude_common_ancestry,
312
'signature': signature,
314
281
# Add 'private' attributes for features that may be deprecated
315
282
'_match_using_deltas': _match_using_deltas,
319
286
def _apply_log_request_defaults(rqst):
320
287
"""Apply default values to a request dictionary."""
321
result = _DEFAULT_REQUEST_PARAMS.copy()
288
result = _DEFAULT_REQUEST_PARAMS
323
290
result.update(rqst)
327
def format_signature_validity(rev_id, repo):
328
"""get the signature validity
330
:param rev_id: revision id to validate
331
:param repo: repository of revision
332
:return: human readable string to print to log
334
from bzrlib import gpg
336
gpg_strategy = gpg.GPGStrategy(None)
337
result = repo.verify_revision(rev_id, gpg_strategy)
338
if result[0] == gpg.SIGNATURE_VALID:
339
return "valid signature from {0}".format(result[1])
340
if result[0] == gpg.SIGNATURE_KEY_MISSING:
341
return "unknown key {0}".format(result[1])
342
if result[0] == gpg.SIGNATURE_NOT_VALID:
343
return "invalid signature!"
344
if result[0] == gpg.SIGNATURE_NOT_SIGNED:
345
return "no signature"
348
294
class LogGenerator(object):
349
295
"""A generator of log revisions."""
395
341
# Tweak the LogRequest based on what the LogFormatter can handle.
396
342
# (There's no point generating stuff if the formatter can't display it.)
398
if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
399
# user didn't specify levels, use whatever the LF can handle:
400
rqst['levels'] = lf.get_levels()
344
rqst['levels'] = lf.get_levels()
402
345
if not getattr(lf, 'supports_tags', False):
403
346
rqst['generate_tags'] = False
404
347
if not getattr(lf, 'supports_delta', False):
405
348
rqst['delta_type'] = None
406
349
if not getattr(lf, 'supports_diff', False):
407
350
rqst['diff_type'] = None
408
if not getattr(lf, 'supports_signatures', False):
409
rqst['signature'] = False
411
352
# Find and print the interesting revisions
412
353
generator = self._generator_factory(self.branch, rqst)
443
384
:return: An iterator yielding LogRevision objects.
446
levels = rqst.get('levels')
447
limit = rqst.get('limit')
448
diff_type = rqst.get('diff_type')
449
show_signature = rqst.get('signature')
451
388
revision_iterator = self._create_log_revision_iterator()
452
389
for revs in revision_iterator:
453
390
for (rev_id, revno, merge_depth), rev, delta in revs:
454
391
# 0 levels means show everything; merge_depth counts from 0
392
levels = rqst.get('levels')
455
393
if levels != 0 and merge_depth >= levels:
457
if diff_type is None:
460
diff = self._format_diff(rev, rev_id, diff_type)
462
signature = format_signature_validity(rev_id,
463
self.branch.repository)
395
diff = self._format_diff(rev, rev_id)
466
396
yield LogRevision(rev, revno, merge_depth, delta,
467
self.rev_tag_dict.get(rev_id), diff, signature)
397
self.rev_tag_dict.get(rev_id), diff)
398
limit = rqst.get('limit')
470
401
if log_count >= limit:
473
def _format_diff(self, rev, rev_id, diff_type):
404
def _format_diff(self, rev, rev_id):
405
diff_type = self.rqst.get('diff_type')
406
if diff_type is None:
474
408
repo = self.branch.repository
475
409
if len(rev.parent_ids) == 0:
476
410
ancestor_id = _mod_revision.NULL_REVISION
516
449
generate_merge_revisions = rqst.get('levels') != 1
517
450
delayed_graph_generation = not rqst.get('specific_fileids') and (
518
451
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
519
view_revisions = _calc_view_revisions(
520
self.branch, self.start_rev_id, self.end_rev_id,
521
rqst.get('direction'),
522
generate_merge_revisions=generate_merge_revisions,
523
delayed_graph_generation=delayed_graph_generation,
524
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
452
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
453
self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
454
delayed_graph_generation=delayed_graph_generation)
526
456
# Apply the other filters
527
457
return make_log_rev_iterator(self.branch, view_revisions,
528
rqst.get('delta_type'), rqst.get('match'),
458
rqst.get('delta_type'), rqst.get('message_search'),
529
459
file_ids=rqst.get('specific_fileids'),
530
460
direction=rqst.get('direction'))
534
464
# Note that we always generate the merge revisions because
535
465
# filter_revisions_touching_file_id() requires them ...
537
view_revisions = _calc_view_revisions(
538
self.branch, self.start_rev_id, self.end_rev_id,
539
rqst.get('direction'), generate_merge_revisions=True,
540
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
467
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
468
self.end_rev_id, rqst.get('direction'), True)
541
469
if not isinstance(view_revisions, list):
542
470
view_revisions = list(view_revisions)
543
471
view_revisions = _filter_revisions_touching_file_id(self.branch,
544
472
rqst.get('specific_fileids')[0], view_revisions,
545
473
include_merges=rqst.get('levels') != 1)
546
474
return make_log_rev_iterator(self.branch, view_revisions,
547
rqst.get('delta_type'), rqst.get('match'))
475
rqst.get('delta_type'), rqst.get('message_search'))
550
478
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
551
generate_merge_revisions,
552
delayed_graph_generation=False,
553
exclude_common_ancestry=False,
479
generate_merge_revisions, delayed_graph_generation=False):
555
480
"""Calculate the revisions to view.
557
482
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
558
483
a list of the same tuples.
560
if (exclude_common_ancestry and start_rev_id == end_rev_id):
561
raise errors.BzrCommandError(
562
'--exclude-common-ancestry requires two different revisions')
563
if direction not in ('reverse', 'forward'):
564
raise ValueError('invalid direction %r' % direction)
565
485
br_revno, br_rev_id = branch.last_revision_info()
566
486
if br_revno == 0:
569
if (end_rev_id and start_rev_id == end_rev_id
570
and (not generate_merge_revisions
571
or not _has_merges(branch, end_rev_id))):
572
# If a single revision is requested, check we can handle it
573
iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
575
elif not generate_merge_revisions:
576
# If we only want to see linear revisions, we can iterate ...
577
iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
578
direction, exclude_common_ancestry)
579
if direction == 'forward':
580
iter_revs = reversed(iter_revs)
489
# If a single revision is requested, check we can handle it
490
generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
491
(not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
492
if generate_single_revision:
493
return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
495
# If we only want to see linear revisions, we can iterate ...
496
if not generate_merge_revisions:
497
return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
582
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
583
direction, delayed_graph_generation,
584
exclude_common_ancestry)
585
if direction == 'forward':
586
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
500
return _generate_all_revisions(branch, start_rev_id, end_rev_id,
501
direction, delayed_graph_generation)
590
504
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
593
507
return [(br_rev_id, br_revno, 0)]
595
revno_str = _compute_revno_str(branch, rev_id)
509
revno = branch.revision_id_to_dotted_revno(rev_id)
510
revno_str = '.'.join(str(n) for n in revno)
596
511
return [(rev_id, revno_str, 0)]
599
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
600
exclude_common_ancestry=False):
601
result = _linear_view_revisions(
602
branch, start_rev_id, end_rev_id,
603
exclude_common_ancestry=exclude_common_ancestry)
514
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
515
result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
604
516
# If a start limit was given and it's not obviously an
605
517
# ancestor of the end limit, check it before outputting anything
606
518
if direction == 'forward' or (start_rev_id
610
522
except _StartNotLinearAncestor:
611
523
raise errors.BzrCommandError('Start revision not found in'
612
524
' left-hand history of end revision.')
525
if direction == 'forward':
526
result = reversed(result)
616
530
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
617
delayed_graph_generation,
618
exclude_common_ancestry=False):
531
delayed_graph_generation):
619
532
# On large trees, generating the merge graph can take 30-60 seconds
620
533
# so we delay doing it until a merge is detected, incrementally
621
534
# returning initial (non-merge) revisions while we can.
623
# The above is only true for old formats (<= 0.92), for newer formats, a
624
# couple of seconds only should be needed to load the whole graph and the
625
# other graph operations needed are even faster than that -- vila 100201
626
535
initial_revisions = []
627
536
if delayed_graph_generation:
629
for rev_id, revno, depth in _linear_view_revisions(
630
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
538
for rev_id, revno, depth in \
539
_linear_view_revisions(branch, start_rev_id, end_rev_id):
631
540
if _has_merges(branch, rev_id):
632
# The end_rev_id can be nested down somewhere. We need an
633
# explicit ancestry check. There is an ambiguity here as we
634
# may not raise _StartNotLinearAncestor for a revision that
635
# is an ancestor but not a *linear* one. But since we have
636
# loaded the graph to do the check (or calculate a dotted
637
# revno), we may as well accept to show the log... We need
638
# the check only if start_rev_id is not None as all
639
# revisions have _mod_revision.NULL_REVISION as an ancestor
641
graph = branch.repository.get_graph()
642
if (start_rev_id is not None
643
and not graph.is_ancestor(start_rev_id, end_rev_id)):
644
raise _StartNotLinearAncestor()
645
# Since we collected the revisions so far, we need to
647
541
end_rev_id = rev_id
650
544
initial_revisions.append((rev_id, revno, depth))
652
546
# No merged revisions found
653
return initial_revisions
547
if direction == 'reverse':
548
return initial_revisions
549
elif direction == 'forward':
550
return reversed(initial_revisions)
552
raise ValueError('invalid direction %r' % direction)
654
553
except _StartNotLinearAncestor:
655
554
# A merge was never detected so the lower revision limit can't
656
555
# be nested down somewhere
657
556
raise errors.BzrCommandError('Start revision not found in'
658
557
' history of end revision.')
660
# We exit the loop above because we encounter a revision with merges, from
661
# this revision, we need to switch to _graph_view_revisions.
663
559
# A log including nested merges is required. If the direction is reverse,
664
560
# we rebase the initial merge depths so that the development line is
665
561
# shown naturally, i.e. just like it is for linear logging. We can easily
667
563
# indented at the end seems slightly nicer in that case.
668
564
view_revisions = chain(iter(initial_revisions),
669
565
_graph_view_revisions(branch, start_rev_id, end_rev_id,
670
rebase_initial_depths=(direction == 'reverse'),
671
exclude_common_ancestry=exclude_common_ancestry))
672
return view_revisions
566
rebase_initial_depths=direction == 'reverse'))
567
if direction == 'reverse':
568
return view_revisions
569
elif direction == 'forward':
570
# Forward means oldest first, adjusting for depth.
571
view_revisions = reverse_by_depth(list(view_revisions))
572
return _rebase_merge_depth(view_revisions)
574
raise ValueError('invalid direction %r' % direction)
675
577
def _has_merges(branch, rev_id):
678
580
return len(parents) > 1
681
def _compute_revno_str(branch, rev_id):
682
"""Compute the revno string from a rev_id.
684
:return: The revno string, or None if the revision is not in the supplied
688
revno = branch.revision_id_to_dotted_revno(rev_id)
689
except errors.NoSuchRevision:
690
# The revision must be outside of this branch
693
return '.'.join(str(n) for n in revno)
696
583
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
697
584
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
698
585
if start_rev_id and end_rev_id:
700
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
701
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
702
except errors.NoSuchRevision:
703
# one or both is not in the branch; not obvious
586
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
587
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
705
588
if len(start_dotted) == 1 and len(end_dotted) == 1:
706
589
# both on mainline
707
590
return start_dotted[0] <= end_dotted[0]
715
# if either start or end is not specified then we use either the first or
716
# the last revision and *they* are obvious ancestors.
720
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
721
exclude_common_ancestry=False):
601
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
722
602
"""Calculate a sequence of revisions to view, newest to oldest.
724
604
:param start_rev_id: the lower revision-id
725
605
:param end_rev_id: the upper revision-id
726
:param exclude_common_ancestry: Whether the start_rev_id should be part of
727
the iterated revisions.
728
606
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
729
607
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
730
is not found walking the left-hand history
608
is not found walking the left-hand history
732
610
br_revno, br_rev_id = branch.last_revision_info()
733
611
repo = branch.repository
734
graph = repo.get_graph()
735
612
if start_rev_id is None and end_rev_id is None:
736
613
cur_revno = br_revno
737
for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
738
(_mod_revision.NULL_REVISION,)):
614
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
739
615
yield revision_id, str(cur_revno), 0
742
618
if end_rev_id is None:
743
619
end_rev_id = br_rev_id
744
620
found_start = start_rev_id is None
745
for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
746
(_mod_revision.NULL_REVISION,)):
747
revno_str = _compute_revno_str(branch, revision_id)
621
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
622
revno = branch.revision_id_to_dotted_revno(revision_id)
623
revno_str = '.'.join(str(n) for n in revno)
748
624
if not found_start and revision_id == start_rev_id:
749
if not exclude_common_ancestry:
750
yield revision_id, revno_str, 0
625
yield revision_id, revno_str, 0
751
626
found_start = True
792
662
depth_adjustment = merge_depth
793
663
if depth_adjustment:
794
664
if merge_depth < depth_adjustment:
795
# From now on we reduce the depth adjustement, this can be
796
# surprising for users. The alternative requires two passes
797
# which breaks the fast display of the first revision
799
665
depth_adjustment = merge_depth
800
666
merge_depth -= depth_adjustment
801
667
yield rev_id, '.'.join(map(str, revno)), merge_depth
670
def calculate_view_revisions(branch, start_revision, end_revision, direction,
671
specific_fileid, generate_merge_revisions):
672
"""Calculate the revisions to view.
674
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
675
a list of the same tuples.
677
# This method is no longer called by the main code path.
678
# It is retained for API compatibility and may be deprecated
680
start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
682
view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
683
direction, generate_merge_revisions or specific_fileid))
685
view_revisions = _filter_revisions_touching_file_id(branch,
686
specific_fileid, view_revisions,
687
include_merges=generate_merge_revisions)
688
return _rebase_merge_depth(view_revisions)
804
691
def _rebase_merge_depth(view_revisions):
805
692
"""Adjust depths upwards so the top level is 0."""
806
693
# If either the first or last revision have a merge_depth of 0, we're done
850
737
return log_rev_iterator
853
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
740
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
854
741
"""Create a filtered iterator of log_rev_iterator matching on a regex.
856
743
:param branch: The branch being logged.
857
744
:param generate_delta: Whether to generate a delta for each revision.
858
:param match: A dictionary with properties as keys and lists of strings
859
as values. To match, a revision may match any of the supplied strings
860
within a single property but must match at least one string for each
745
:param search: A user text search string.
862
746
:param log_rev_iterator: An input iterator containing all revisions that
863
747
could be displayed, in lists.
864
748
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
868
752
return log_rev_iterator
869
searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
870
for (k,v) in match.iteritems()]
871
return _filter_re(searchRE, log_rev_iterator)
874
def _filter_re(searchRE, log_rev_iterator):
753
searchRE = re_compile_checked(search, re.IGNORECASE,
754
'log message filter')
755
return _filter_message_re(searchRE, log_rev_iterator)
758
def _filter_message_re(searchRE, log_rev_iterator):
875
759
for revs in log_rev_iterator:
876
new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
880
def _match_filter(searchRE, rev):
882
'message': (rev.message,),
883
'committer': (rev.committer,),
884
'author': (rev.get_apparent_authors()),
885
'bugs': list(rev.iter_bugs())
887
strings[''] = [item for inner_list in strings.itervalues()
888
for item in inner_list]
889
for (k,v) in searchRE:
890
if k in strings and not _match_any_filter(strings[k], v):
894
def _match_any_filter(strings, res):
895
return any([filter(None, map(re.search, strings)) for re in res])
761
for (rev_id, revno, merge_depth), rev, delta in revs:
762
if searchRE.search(rev.message):
763
new_revs.append(((rev_id, revno, merge_depth), rev, delta))
897
767
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
898
768
fileids=None, direction='reverse'):
1162
1032
return mainline_revs, rev_nos, start_rev_id, end_rev_id
1035
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1036
"""Filter view_revisions based on revision ranges.
1038
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1039
tuples to be filtered.
1041
:param start_rev_id: If not NONE specifies the first revision to be logged.
1042
If NONE then all revisions up to the end_rev_id are logged.
1044
:param end_rev_id: If not NONE specifies the last revision to be logged.
1045
If NONE then all revisions up to the end of the log are logged.
1047
:return: The filtered view_revisions.
1049
# This method is no longer called by the main code path.
1050
# It may be removed soon. IGC 20090127
1051
if start_rev_id or end_rev_id:
1052
revision_ids = [r for r, n, d in view_revisions]
1054
start_index = revision_ids.index(start_rev_id)
1057
if start_rev_id == end_rev_id:
1058
end_index = start_index
1061
end_index = revision_ids.index(end_rev_id)
1063
end_index = len(view_revisions) - 1
1064
# To include the revisions merged into the last revision,
1065
# extend end_rev_id down to, but not including, the next rev
1066
# with the same or lesser merge_depth
1067
end_merge_depth = view_revisions[end_index][2]
1069
for index in xrange(end_index+1, len(view_revisions)+1):
1070
if view_revisions[index][2] <= end_merge_depth:
1071
end_index = index - 1
1074
# if the search falls off the end then log to the end as well
1075
end_index = len(view_revisions) - 1
1076
view_revisions = view_revisions[start_index:end_index+1]
1077
return view_revisions
1165
1080
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1166
1081
include_merges=True):
1167
1082
r"""Return the list of revision ids which touch a given file id.
1162
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1163
include_merges=True):
1164
"""Produce an iterator of revisions to show
1165
:return: an iterator of (revision_id, revno, merge_depth)
1166
(if there is no revno for a revision, None is supplied)
1168
# This method is no longer called by the main code path.
1169
# It is retained for API compatibility and may be deprecated
1170
# soon. IGC 20090127
1171
if not include_merges:
1172
revision_ids = mainline_revs[1:]
1173
if direction == 'reverse':
1174
revision_ids.reverse()
1175
for revision_id in revision_ids:
1176
yield revision_id, str(rev_nos[revision_id]), 0
1178
graph = branch.repository.get_graph()
1179
# This asks for all mainline revisions, which means we only have to spider
1180
# sideways, rather than depth history. That said, its still size-of-history
1181
# and should be addressed.
1182
# mainline_revisions always includes an extra revision at the beginning, so
1184
parent_map = dict(((key, value) for key, value in
1185
graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1186
# filter out ghosts; merge_sort errors on ghosts.
1187
rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1188
merge_sorted_revisions = tsort.merge_sort(
1192
generate_revno=True)
1194
if direction == 'forward':
1195
# forward means oldest first.
1196
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1197
elif direction != 'reverse':
1198
raise ValueError('invalid direction %r' % direction)
1200
for (sequence, rev_id, merge_depth, revno, end_of_merge
1201
) in merge_sorted_revisions:
1202
yield rev_id, '.'.join(map(str, revno)), merge_depth
1249
1205
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1250
1206
"""Reverse revisions by depth.
1311
1263
to indicate which LogRevision attributes it supports:
1313
1265
- supports_delta must be True if this log formatter supports delta.
1314
Otherwise the delta attribute may not be populated. The 'delta_format'
1315
attribute describes whether the 'short_status' format (1) or the long
1316
one (2) should be used.
1266
Otherwise the delta attribute may not be populated. The 'delta_format'
1267
attribute describes whether the 'short_status' format (1) or the long
1268
one (2) should be used.
1318
1270
- supports_merge_revisions must be True if this log formatter supports
1319
merge revisions. If not, then only mainline revisions will be passed
1271
merge revisions. If not, then only mainline revisions will be passed
1322
1274
- preferred_levels is the number of levels this formatter defaults to.
1323
The default value is zero meaning display all levels.
1324
This value is only relevant if supports_merge_revisions is True.
1275
The default value is zero meaning display all levels.
1276
This value is only relevant if supports_merge_revisions is True.
1326
1278
- supports_tags must be True if this log formatter supports tags.
1327
Otherwise the tags attribute may not be populated.
1279
Otherwise the tags attribute may not be populated.
1329
1281
- supports_diff must be True if this log formatter supports diffs.
1330
Otherwise the diff attribute may not be populated.
1332
- supports_signatures must be True if this log formatter supports GPG
1282
Otherwise the diff attribute may not be populated.
1335
1284
Plugins can register functions to show custom revision properties using
1336
1285
the properties_handler_registry. The registered function
1337
must respect the following interface description::
1286
must respect the following interface description:
1339
1287
def my_show_properties(properties_dict):
1340
1288
# code that returns a dict {'name':'value'} of the properties
1343
1291
preferred_levels = 0
1345
1293
def __init__(self, to_file, show_ids=False, show_timezone='original',
1346
delta_format=None, levels=None, show_advice=False,
1347
to_exact_file=None, author_list_handler=None):
1294
delta_format=None, levels=None, show_advice=False):
1348
1295
"""Create a LogFormatter.
1350
1297
:param to_file: the file to output to
1351
:param to_exact_file: if set, gives an output stream to which
1352
non-Unicode diffs are written.
1353
1298
:param show_ids: if True, revision-ids are to be displayed
1354
1299
:param show_timezone: the timezone to use
1355
1300
:param delta_format: the level of delta information to display
1358
1303
let the log formatter decide.
1359
1304
:param show_advice: whether to show advice at the end of the
1361
:param author_list_handler: callable generating a list of
1362
authors to display for a given revision
1364
1307
self.to_file = to_file
1365
1308
# 'exact' stream used to show diff, it should print content 'as is'
1366
1309
# and should not try to decode/encode it to unicode to avoid bug #328007
1367
if to_exact_file is not None:
1368
self.to_exact_file = to_exact_file
1370
# XXX: somewhat hacky; this assumes it's a codec writer; it's better
1371
# for code that expects to get diffs to pass in the exact file
1373
self.to_exact_file = getattr(to_file, 'stream', to_file)
1310
self.to_exact_file = getattr(to_file, 'stream', to_file)
1374
1311
self.show_ids = show_ids
1375
1312
self.show_timezone = show_timezone
1376
1313
if delta_format is None:
1420
1356
def short_author(self, rev):
1421
return self.authors(rev, 'first', short=True, sep=', ')
1423
def authors(self, rev, who, short=False, sep=None):
1424
"""Generate list of authors, taking --authors option into account.
1426
The caller has to specify the name of a author list handler,
1427
as provided by the author list registry, using the ``who``
1428
argument. That name only sets a default, though: when the
1429
user selected a different author list generation using the
1430
``--authors`` command line switch, as represented by the
1431
``author_list_handler`` constructor argument, that value takes
1434
:param rev: The revision for which to generate the list of authors.
1435
:param who: Name of the default handler.
1436
:param short: Whether to shorten names to either name or address.
1437
:param sep: What separator to use for automatic concatenation.
1439
if self._author_list_handler is not None:
1440
# The user did specify --authors, which overrides the default
1441
author_list_handler = self._author_list_handler
1443
# The user didn't specify --authors, so we use the caller's default
1444
author_list_handler = author_list_registry.get(who)
1445
names = author_list_handler(rev)
1447
for i in range(len(names)):
1448
name, address = config.parse_username(names[i])
1454
names = sep.join(names)
1357
name, address = config.parse_username(rev.get_apparent_authors()[0])
1457
1362
def merge_marker(self, revision):
1458
1363
"""Get the merge marker to include in the output or '' if none."""
1465
def show_properties(self, revision, indent):
1466
"""Displays the custom properties returned by each registered handler.
1468
If a registered handler raises an error it is propagated.
1470
for line in self.custom_properties(revision):
1471
self.to_file.write("%s%s\n" % (indent, line))
1473
def custom_properties(self, revision):
1474
"""Format the custom properties returned by each registered handler.
1476
If a registered handler raises an error it is propagated.
1478
:return: a list of formatted lines (excluding trailing newlines)
1480
lines = self._foreign_info_properties(revision)
1481
for key, handler in properties_handler_registry.iteritems():
1482
lines.extend(self._format_properties(handler(revision)))
1485
def _foreign_info_properties(self, rev):
1370
def show_foreign_info(self, rev, indent):
1486
1371
"""Custom log displayer for foreign revision identifiers.
1488
1373
:param rev: Revision object.
1490
1375
# Revision comes directly from a foreign repository
1491
1376
if isinstance(rev, foreign.ForeignRevision):
1492
return self._format_properties(
1493
rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1377
self._write_properties(indent, rev.mapping.vcs.show_foreign_revid(
1495
1381
# Imported foreign revision revision ids always contain :
1496
1382
if not ":" in rev.revision_id:
1499
1385
# Revision was once imported from a foreign repository
1501
1387
foreign_revid, mapping = \
1502
1388
foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1503
1389
except errors.InvalidRevisionId:
1506
return self._format_properties(
1392
self._write_properties(indent,
1507
1393
mapping.vcs.show_foreign_revid(foreign_revid))
1509
def _format_properties(self, properties):
1395
def show_properties(self, revision, indent):
1396
"""Displays the custom properties returned by each registered handler.
1398
If a registered handler raises an error it is propagated.
1400
for key, handler in properties_handler_registry.iteritems():
1401
self._write_properties(indent, handler(revision))
1403
def _write_properties(self, indent, properties):
1511
1404
for key, value in properties.items():
1512
lines.append(key + ': ' + value)
1405
self.to_file.write(indent + key + ': ' + value + '\n')
1515
1407
def show_diff(self, to_file, diff, indent):
1516
1408
for l in diff.rstrip().split('\n'):
1517
1409
to_file.write(indent + '%s\n' % (l,))
1520
# Separator between revisions in long format
1521
_LONG_SEP = '-' * 60
1524
1412
class LongLogFormatter(LogFormatter):
1526
1414
supports_merge_revisions = True
1528
1416
supports_delta = True
1529
1417
supports_tags = True
1530
1418
supports_diff = True
1531
supports_signatures = True
1533
def __init__(self, *args, **kwargs):
1534
super(LongLogFormatter, self).__init__(*args, **kwargs)
1535
if self.show_timezone == 'original':
1536
self.date_string = self._date_string_original_timezone
1538
self.date_string = self._date_string_with_timezone
1540
def _date_string_with_timezone(self, rev):
1541
return format_date(rev.timestamp, rev.timezone or 0,
1544
def _date_string_original_timezone(self, rev):
1545
return format_date_with_offset_in_original_timezone(rev.timestamp,
1548
1420
def log_revision(self, revision):
1549
1421
"""Log a revision, either merged or not."""
1550
1422
indent = ' ' * revision.merge_depth
1423
to_file = self.to_file
1424
to_file.write(indent + '-' * 60 + '\n')
1552
1425
if revision.revno is not None:
1553
lines.append('revno: %s%s' % (revision.revno,
1426
to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
1554
1427
self.merge_marker(revision)))
1555
1428
if revision.tags:
1556
lines.append('tags: %s' % (', '.join(revision.tags)))
1557
if self.show_ids or revision.revno is None:
1558
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1429
to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1559
1430
if self.show_ids:
1431
to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1560
1433
for parent_id in revision.rev.parent_ids:
1561
lines.append('parent: %s' % (parent_id,))
1562
lines.extend(self.custom_properties(revision.rev))
1434
to_file.write(indent + 'parent: %s\n' % (parent_id,))
1435
self.show_foreign_info(revision.rev, indent)
1436
self.show_properties(revision.rev, indent)
1564
1438
committer = revision.rev.committer
1565
authors = self.authors(revision.rev, 'all')
1439
authors = revision.rev.get_apparent_authors()
1566
1440
if authors != [committer]:
1567
lines.append('author: %s' % (", ".join(authors),))
1568
lines.append('committer: %s' % (committer,))
1441
to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1442
to_file.write(indent + 'committer: %s\n' % (committer,))
1570
1444
branch_nick = revision.rev.properties.get('branch-nick', None)
1571
1445
if branch_nick is not None:
1572
lines.append('branch nick: %s' % (branch_nick,))
1574
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1576
if revision.signature is not None:
1577
lines.append('signature: ' + revision.signature)
1579
lines.append('message:')
1446
to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
1448
date_str = format_date(revision.rev.timestamp,
1449
revision.rev.timezone or 0,
1451
to_file.write(indent + 'timestamp: %s\n' % (date_str,))
1453
to_file.write(indent + 'message:\n')
1580
1454
if not revision.rev.message:
1581
lines.append(' (no message)')
1455
to_file.write(indent + ' (no message)\n')
1583
1457
message = revision.rev.message.rstrip('\r\n')
1584
1458
for l in message.split('\n'):
1585
lines.append(' %s' % (l,))
1587
# Dump the output, appending the delta and diff if requested
1588
to_file = self.to_file
1589
to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1459
to_file.write(indent + ' %s\n' % (l,))
1590
1460
if revision.delta is not None:
1591
# Use the standard status output to display changes
1592
from bzrlib.delta import report_delta
1593
report_delta(to_file, revision.delta, short_status=False,
1594
show_ids=self.show_ids, indent=indent)
1461
# We don't respect delta_format for compatibility
1462
revision.delta.show(to_file, self.show_ids, indent=indent,
1595
1464
if revision.diff is not None:
1596
1465
to_file.write(indent + 'diff:\n')
1598
1466
# Note: we explicitly don't indent the diff (relative to the
1599
1467
# revision information) so that the output can be fed to patch -p0
1600
1468
self.show_diff(self.to_exact_file, revision.diff, indent)
1601
self.to_exact_file.flush()
1603
1470
def get_advice_separator(self):
1604
1471
"""Get the text separating the log from the closing advice."""
1642
1509
if revision.tags:
1643
1510
tags = ' {%s}' % (', '.join(revision.tags))
1644
1511
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1645
revision.revno or "", self.short_author(revision.rev),
1512
revision.revno, self.short_author(revision.rev),
1646
1513
format_date(revision.rev.timestamp,
1647
1514
revision.rev.timezone or 0,
1648
1515
self.show_timezone, date_fmt="%Y-%m-%d",
1649
1516
show_offset=False),
1650
1517
tags, self.merge_marker(revision)))
1518
self.show_foreign_info(revision.rev, indent+offset)
1651
1519
self.show_properties(revision.rev, indent+offset)
1652
if self.show_ids or revision.revno is None:
1653
1521
to_file.write(indent + offset + 'revision-id:%s\n'
1654
1522
% (revision.rev.revision_id,))
1655
1523
if not revision.rev.message:
1709
1570
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1710
1571
"""Format log info into one string. Truncate tail of 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
1572
:param revno: revision number or None.
1573
Revision numbers counts from 1.
1574
:param rev: revision object
1575
:param max_chars: maximum length of resulting string
1576
:param tags: list of tags or None
1577
:param prefix: string to prefix each line
1578
:return: formatted truncated string
1722
1582
# show revno only when is not None
1723
1583
out.append("%s:" % revno)
1724
if max_chars is not None:
1725
out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1727
out.append(self.short_author(rev))
1584
out.append(self.truncate(self.short_author(rev), 20))
1728
1585
out.append(self.date_string(rev))
1729
1586
if len(rev.parent_ids) > 1:
1730
1587
out.append('[merge]')
1821
1677
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1824
def author_list_all(rev):
1825
return rev.get_apparent_authors()[:]
1828
def author_list_first(rev):
1829
lst = rev.get_apparent_authors()
1836
def author_list_committer(rev):
1837
return [rev.committer]
1840
author_list_registry = registry.Registry()
1842
author_list_registry.register('all', author_list_all,
1845
author_list_registry.register('first', author_list_first,
1848
author_list_registry.register('committer', author_list_committer,
1680
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1681
# deprecated; for compatibility
1682
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1683
lf.show(revno, rev, delta)
1852
1686
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
2010
1843
:param file_list: the list of paths given on the command line;
2011
1844
the first of these can be a branch location or a file path,
2012
1845
the remainder must be file paths
2013
:param add_cleanup: When the branch returned is read locked,
2014
an unlock call will be queued to the cleanup.
2015
1846
:return: (branch, info_list, start_rev_info, end_rev_info) where
2016
1847
info_list is a list of (relative_path, file_id, kind) tuples where
2017
1848
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2018
branch will be read-locked.
2020
from builtins import _get_revision_range
1850
from builtins import _get_revision_range, safe_relpath_files
2021
1851
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2022
add_cleanup(b.lock_read().unlock)
2023
1852
# XXX: It's damn messy converting a list of paths to relative paths when
2024
1853
# those paths might be deleted ones, they might be on a case-insensitive
2025
1854
# filesystem and/or they might be in silly locations (like another branch).