103
71
TODO: Perhaps some way to limit this to only particular revisions,
104
72
or to traverse a non-mainline set of revisions?
106
last_verifier = last_tree.get_file_verifier(last_path)
107
graph = repository.get_graph()
108
history = list(graph.iter_lefthand_ancestry(last_revision, []))
110
for revision_id in history:
111
this_tree = repository.revision_tree(revision_id)
112
this_intertree = InterTree.get(this_tree, last_tree)
113
this_path = this_intertree.find_source_path(last_path)
77
for revision_id in branch.revision_history():
78
this_inv = branch.repository.get_revision_inventory(revision_id)
79
if file_id in this_inv:
80
this_ie = this_inv[file_id]
81
this_path = this_inv.id2path(file_id)
83
this_ie = this_path = None
115
85
# now we know how it was last time, and how it is in this revision.
116
86
# are those two states effectively the same or not?
117
if this_path is not None and last_path is None:
118
yield revno, revision_id, "deleted " + this_path
119
this_verifier = this_tree.get_file_verifier(this_path)
120
elif this_path is None and last_path is not None:
121
yield revno, revision_id, "added " + last_path
88
if not this_ie and not last_ie:
89
# not present in either
91
elif this_ie and not last_ie:
92
yield revno, revision_id, "added " + this_path
93
elif not this_ie and last_ie:
95
yield revno, revision_id, "deleted " + last_path
122
96
elif this_path != last_path:
123
yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
124
this_verifier = this_tree.get_file_verifier(this_path)
126
this_verifier = this_tree.get_file_verifier(this_path)
127
if (this_verifier != last_verifier):
128
yield revno, revision_id, "modified " + this_path
97
yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
98
elif (this_ie.text_size != last_ie.text_size
99
or this_ie.text_sha1 != last_ie.text_sha1):
100
yield revno, revision_id, "modified " + this_path
130
last_verifier = this_verifier
131
103
last_path = this_path
132
last_tree = this_tree
133
if last_path is None:
108
def _enumerate_history(branch):
111
for rev_id in branch.revision_history():
112
rh.append((revno, rev_id))
117
def _get_revision_delta(branch, revno):
118
"""Return the delta for a mainline revision.
120
This is used to show summaries in verbose logs, and also for finding
121
revisions which touch a given file."""
122
# XXX: What are we supposed to do when showing a summary for something
123
# other than a mainline revision. The delta to it's first parent, or
124
# (more useful) the delta to a nominated other revision.
125
return branch.get_revision_delta(revno)
138
128
def show_log(branch,
130
specific_fileid=None,
141
132
direction='reverse',
142
133
start_revision=None,
143
134
end_revision=None,
148
136
"""Write out human-readable log of commits to this branch.
150
This function is being retained for backwards compatibility but
151
should not be extended with new parameters. Use the new Logger class
152
instead, eg. Logger(branch, rqst).show(lf), adding parameters to the
153
make_log_request_dict function.
155
:param lf: The LogFormatter object showing the output.
157
:param verbose: If True show added/changed/deleted/renamed files.
159
:param direction: 'reverse' (default) is latest to earliest; 'forward' is
162
:param start_revision: If not None, only show revisions >= start_revision
164
:param end_revision: If not None, only show revisions <= end_revision
166
:param search: If not None, only show revisions with matching commit
169
:param limit: If set, shows only 'limit' revisions, all revisions are shown
172
:param show_diff: If True, output a diff after each revision.
174
:param match: Dictionary of search lists to use when matching revision
186
if isinstance(start_revision, int):
188
start_revision = revisionspec.RevisionInfo(branch, start_revision)
189
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
190
raise errors.InvalidRevisionNumber(start_revision)
192
if isinstance(end_revision, int):
194
end_revision = revisionspec.RevisionInfo(branch, end_revision)
195
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
196
raise errors.InvalidRevisionNumber(end_revision)
198
if end_revision is not None and end_revision.revno == 0:
199
raise errors.InvalidRevisionNumber(end_revision.revno)
201
# Build the request and execute it
202
rqst = make_log_request_dict(
204
start_revision=start_revision, end_revision=end_revision,
205
limit=limit, message_search=search,
206
delta_type=delta_type, diff_type=diff_type)
207
Logger(branch, rqst).show(lf)
210
# Note: This needs to be kept in sync with the defaults in
211
# make_log_request_dict() below
212
_DEFAULT_REQUEST_PARAMS = {
213
'direction': 'reverse',
215
'generate_tags': True,
216
'exclude_common_ancestry': False,
217
'_match_using_deltas': True,
221
def make_log_request_dict(direction='reverse', specific_fileids=None,
222
start_revision=None, end_revision=None, limit=None,
223
message_search=None, levels=None, generate_tags=True,
225
diff_type=None, _match_using_deltas=True,
226
exclude_common_ancestry=False, match=None,
227
signature=False, omit_merges=False,
229
"""Convenience function for making a logging request dictionary.
231
Using this function may make code slightly safer by ensuring
232
parameters have the correct names. It also provides a reference
233
point for documenting the supported parameters.
235
:param direction: 'reverse' (default) is latest to earliest;
236
'forward' is earliest to latest.
238
:param specific_fileids: If not None, only include revisions
239
affecting the specified files, rather than all revisions.
241
:param start_revision: If not None, only generate
242
revisions >= start_revision
244
:param end_revision: If not None, only generate
245
revisions <= end_revision
247
:param limit: If set, generate only 'limit' revisions, all revisions
248
are shown if None or 0.
250
:param message_search: If not None, only include revisions with
251
matching commit messages
253
:param levels: the number of levels of revisions to
254
generate; 1 for just the mainline; 0 for all levels, or None for
257
:param generate_tags: If True, include tags for matched revisions.
259
:param delta_type: Either 'full', 'partial' or None.
260
'full' means generate the complete delta - adds/deletes/modifies/etc;
261
'partial' means filter the delta using specific_fileids;
262
None means do not generate any delta.
264
:param diff_type: Either 'full', 'partial' or None.
265
'full' means generate the complete diff - adds/deletes/modifies/etc;
266
'partial' means filter the diff using specific_fileids;
267
None means do not generate any diff.
269
:param _match_using_deltas: a private parameter controlling the
270
algorithm used for matching specific_fileids. This parameter
271
may be removed in the future so breezy client code should NOT
274
:param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
275
range operator or as a graph difference.
277
:param signature: show digital signature information
279
:param match: Dictionary of list of search strings to use when filtering
280
revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
281
the empty string to match any of the preceding properties.
283
:param omit_merges: If True, commits with more than one parent are
287
# Take care of old style message_search parameter
290
if 'message' in match:
291
match['message'].append(message_search)
293
match['message'] = [message_search]
295
match = {'message': [message_search]}
297
'direction': direction,
298
'specific_fileids': specific_fileids,
299
'start_revision': start_revision,
300
'end_revision': end_revision,
303
'generate_tags': generate_tags,
304
'delta_type': delta_type,
305
'diff_type': diff_type,
306
'exclude_common_ancestry': exclude_common_ancestry,
307
'signature': signature,
309
'omit_merges': omit_merges,
310
# Add 'private' attributes for features that may be deprecated
311
'_match_using_deltas': _match_using_deltas,
315
def _apply_log_request_defaults(rqst):
316
"""Apply default values to a request dictionary."""
317
result = _DEFAULT_REQUEST_PARAMS.copy()
323
def format_signature_validity(rev_id, branch):
324
"""get the signature validity
326
:param rev_id: revision id to validate
327
:param branch: branch of revision
328
:return: human readable string to print to log
330
from breezy import gpg
332
gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
333
result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
334
if result[0] == gpg.SIGNATURE_VALID:
335
return u"valid signature from {0}".format(result[1])
336
if result[0] == gpg.SIGNATURE_KEY_MISSING:
337
return "unknown key {0}".format(result[1])
338
if result[0] == gpg.SIGNATURE_NOT_VALID:
339
return "invalid signature!"
340
if result[0] == gpg.SIGNATURE_NOT_SIGNED:
341
return "no signature"
344
class LogGenerator(object):
345
"""A generator of log revisions."""
347
def iter_log_revisions(self):
348
"""Iterate over LogRevision objects.
350
:return: An iterator yielding LogRevision objects.
352
raise NotImplementedError(self.iter_log_revisions)
355
class Logger(object):
356
"""An object that generates, formats and displays a log."""
358
def __init__(self, branch, rqst):
361
:param branch: the branch to log
362
:param rqst: A dictionary specifying the query parameters.
363
See make_log_request_dict() for supported values.
366
self.rqst = _apply_log_request_defaults(rqst)
371
:param lf: The LogFormatter object to send the output to.
373
if not isinstance(lf, LogFormatter):
374
warn("not a LogFormatter instance: %r" % lf)
376
with self.branch.lock_read():
377
if getattr(lf, 'begin_log', None):
380
if getattr(lf, 'end_log', None):
383
def _show_body(self, lf):
384
"""Show the main log output.
386
Subclasses may wish to override this.
388
# Tweak the LogRequest based on what the LogFormatter can handle.
389
# (There's no point generating stuff if the formatter can't display it.)
391
if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
392
# user didn't specify levels, use whatever the LF can handle:
393
rqst['levels'] = lf.get_levels()
395
if not getattr(lf, 'supports_tags', False):
396
rqst['generate_tags'] = False
397
if not getattr(lf, 'supports_delta', False):
398
rqst['delta_type'] = None
399
if not getattr(lf, 'supports_diff', False):
400
rqst['diff_type'] = None
401
if not getattr(lf, 'supports_signatures', False):
402
rqst['signature'] = False
404
# Find and print the interesting revisions
405
generator = self._generator_factory(self.branch, rqst)
407
for lr in generator.iter_log_revisions():
409
except errors.GhostRevisionUnusableHere:
410
raise errors.BzrCommandError(
411
gettext('Further revision history missing.'))
414
def _generator_factory(self, branch, rqst):
415
"""Make the LogGenerator object to use.
417
Subclasses may wish to override this.
419
return _DefaultLogGenerator(branch, rqst)
422
class _StartNotLinearAncestor(Exception):
423
"""Raised when a start revision is not found walking left-hand history."""
426
class _DefaultLogGenerator(LogGenerator):
427
"""The default generator of log revisions."""
429
def __init__(self, branch, rqst):
432
if rqst.get('generate_tags') and branch.supports_tags():
433
self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
435
self.rev_tag_dict = {}
437
def iter_log_revisions(self):
438
"""Iterate over LogRevision objects.
440
:return: An iterator yielding LogRevision objects.
443
levels = rqst.get('levels')
444
limit = rqst.get('limit')
445
diff_type = rqst.get('diff_type')
446
show_signature = rqst.get('signature')
447
omit_merges = rqst.get('omit_merges')
449
revision_iterator = self._create_log_revision_iterator()
450
for revs in revision_iterator:
451
for (rev_id, revno, merge_depth), rev, delta in revs:
452
# 0 levels means show everything; merge_depth counts from 0
453
if (levels != 0 and merge_depth is not None and
454
merge_depth >= levels):
456
if omit_merges and len(rev.parent_ids) > 1:
459
raise errors.GhostRevisionUnusableHere(rev_id)
460
if diff_type is None:
463
diff = self._format_diff(rev, rev_id, diff_type)
465
signature = format_signature_validity(rev_id, self.branch)
469
rev, revno, merge_depth, delta,
470
self.rev_tag_dict.get(rev_id), diff, signature)
473
if log_count >= limit:
476
def _format_diff(self, rev, rev_id, diff_type):
477
repo = self.branch.repository
478
if len(rev.parent_ids) == 0:
479
ancestor_id = _mod_revision.NULL_REVISION
481
ancestor_id = rev.parent_ids[0]
482
tree_1 = repo.revision_tree(ancestor_id)
483
tree_2 = repo.revision_tree(rev_id)
484
file_ids = self.rqst.get('specific_fileids')
485
if diff_type == 'partial' and file_ids is not None:
486
specific_files = [tree_2.id2path(id) for id in file_ids]
488
specific_files = None
490
path_encoding = get_diff_header_encoding()
491
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
492
new_label='', path_encoding=path_encoding)
495
def _create_log_revision_iterator(self):
496
"""Create a revision iterator for log.
498
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
501
self.start_rev_id, self.end_rev_id = _get_revision_limits(
502
self.branch, self.rqst.get('start_revision'),
503
self.rqst.get('end_revision'))
504
if self.rqst.get('_match_using_deltas'):
505
return self._log_revision_iterator_using_delta_matching()
507
# We're using the per-file-graph algorithm. This scales really
508
# well but only makes sense if there is a single file and it's
510
file_count = len(self.rqst.get('specific_fileids'))
512
raise errors.BzrError(
513
"illegal LogRequest: must match-using-deltas "
514
"when logging %d files" % file_count)
515
return self._log_revision_iterator_using_per_file_graph()
517
def _log_revision_iterator_using_delta_matching(self):
518
# Get the base revisions, filtering by the revision range
520
generate_merge_revisions = rqst.get('levels') != 1
521
delayed_graph_generation = not rqst.get('specific_fileids') and (
522
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
523
view_revisions = _calc_view_revisions(
524
self.branch, self.start_rev_id, self.end_rev_id,
525
rqst.get('direction'),
526
generate_merge_revisions=generate_merge_revisions,
527
delayed_graph_generation=delayed_graph_generation,
528
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
530
# Apply the other filters
531
return make_log_rev_iterator(self.branch, view_revisions,
532
rqst.get('delta_type'), rqst.get('match'),
533
file_ids=rqst.get('specific_fileids'),
534
direction=rqst.get('direction'))
536
def _log_revision_iterator_using_per_file_graph(self):
537
# Get the base revisions, filtering by the revision range.
538
# Note that we always generate the merge revisions because
539
# filter_revisions_touching_file_id() requires them ...
541
view_revisions = _calc_view_revisions(
542
self.branch, self.start_rev_id, self.end_rev_id,
543
rqst.get('direction'), generate_merge_revisions=True,
544
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
545
if not isinstance(view_revisions, list):
546
view_revisions = list(view_revisions)
547
view_revisions = _filter_revisions_touching_file_id(self.branch,
548
rqst.get('specific_fileids')[
550
include_merges=rqst.get('levels') != 1)
551
return make_log_rev_iterator(self.branch, view_revisions,
552
rqst.get('delta_type'), rqst.get('match'))
555
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
556
generate_merge_revisions,
557
delayed_graph_generation=False,
558
exclude_common_ancestry=False,
560
"""Calculate the revisions to view.
562
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
563
a list of the same tuples.
565
if (exclude_common_ancestry and start_rev_id == end_rev_id):
566
raise errors.BzrCommandError(gettext(
567
'--exclude-common-ancestry requires two different revisions'))
568
if direction not in ('reverse', 'forward'):
569
raise ValueError(gettext('invalid direction %r') % direction)
570
br_rev_id = branch.last_revision()
571
if br_rev_id == _mod_revision.NULL_REVISION:
574
if (end_rev_id and start_rev_id == end_rev_id
575
and (not generate_merge_revisions
576
or not _has_merges(branch, end_rev_id))):
577
# If a single revision is requested, check we can handle it
578
return _generate_one_revision(branch, end_rev_id, br_rev_id,
580
if not generate_merge_revisions:
582
# If we only want to see linear revisions, we can iterate ...
583
iter_revs = _linear_view_revisions(
584
branch, start_rev_id, end_rev_id,
585
exclude_common_ancestry=exclude_common_ancestry)
586
# If a start limit was given and it's not obviously an
587
# ancestor of the end limit, check it before outputting anything
588
if (direction == 'forward'
589
or (start_rev_id and not _is_obvious_ancestor(
590
branch, start_rev_id, end_rev_id))):
591
iter_revs = list(iter_revs)
592
if direction == 'forward':
593
iter_revs = reversed(iter_revs)
595
except _StartNotLinearAncestor:
596
# Switch to the slower implementation that may be able to find a
597
# non-obvious ancestor out of the left-hand history.
599
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
600
direction, delayed_graph_generation,
601
exclude_common_ancestry)
602
if direction == 'forward':
603
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
607
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
608
if rev_id == br_rev_id:
610
return [(br_rev_id, br_revno, 0)]
612
revno_str = _compute_revno_str(branch, rev_id)
613
return [(rev_id, revno_str, 0)]
616
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
617
delayed_graph_generation,
618
exclude_common_ancestry=False):
619
# On large trees, generating the merge graph can take 30-60 seconds
620
# so we delay doing it until a merge is detected, incrementally
621
# 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
initial_revisions = []
627
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):
631
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
650
initial_revisions.append((rev_id, revno, depth))
652
# No merged revisions found
653
return initial_revisions
654
except _StartNotLinearAncestor:
655
# A merge was never detected so the lower revision limit can't
656
# be nested down somewhere
657
raise errors.BzrCommandError(gettext('Start revision not found in'
658
' 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
# A log including nested merges is required. If the direction is reverse,
664
# we rebase the initial merge depths so that the development line is
665
# shown naturally, i.e. just like it is for linear logging. We can easily
666
# make forward the exact opposite display, but showing the merge revisions
667
# indented at the end seems slightly nicer in that case.
668
view_revisions = itertools.chain(iter(initial_revisions),
669
_graph_view_revisions(branch, start_rev_id, end_rev_id,
670
rebase_initial_depths=(
671
direction == 'reverse'),
672
exclude_common_ancestry=exclude_common_ancestry))
673
return view_revisions
676
def _has_merges(branch, rev_id):
677
"""Does a revision have multiple parents or not?"""
678
parents = branch.repository.get_parent_map([rev_id]).get(rev_id, [])
679
return len(parents) > 1
682
def _compute_revno_str(branch, rev_id):
683
"""Compute the revno string from a rev_id.
685
:return: The revno string, or None if the revision is not in the supplied
139
LogFormatter object to show the output.
142
If true, list only the commits affecting the specified
143
file, rather than all commits.
146
If true show added/changed/deleted/renamed files.
149
'reverse' (default) is latest to earliest;
150
'forward' is earliest to latest.
153
If not None, only show revisions >= start_revision
156
If not None, only show revisions <= end_revision
689
revno = branch.revision_id_to_dotted_revno(rev_id)
690
except errors.NoSuchRevision:
691
# The revision must be outside of this branch
694
return '.'.join(str(n) for n in revno)
697
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
698
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
699
if start_rev_id and end_rev_id:
701
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
702
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
703
except errors.NoSuchRevision:
704
# one or both is not in the branch; not obvious
706
if len(start_dotted) == 1 and len(end_dotted) == 1:
708
return start_dotted[0] <= end_dotted[0]
709
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
710
start_dotted[0:1] == end_dotted[0:1]):
711
# both on same development line
712
return start_dotted[2] <= end_dotted[2]
716
# if either start or end is not specified then we use either the first or
717
# the last revision and *they* are obvious ancestors.
721
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
722
exclude_common_ancestry=False):
723
"""Calculate a sequence of revisions to view, newest to oldest.
725
:param start_rev_id: the lower revision-id
726
:param end_rev_id: the upper revision-id
727
:param exclude_common_ancestry: Whether the start_rev_id should be part of
728
the iterated revisions.
729
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
730
dotted_revno will be None for ghosts
731
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
732
is not found walking the left-hand history
734
repo = branch.repository
735
graph = repo.get_graph()
736
if start_rev_id is None and end_rev_id is None:
737
if branch._format.stores_revno() or \
738
config.GlobalStack().get('calculate_revnos'):
740
br_revno, br_rev_id = branch.last_revision_info()
741
except errors.GhostRevisionsHaveNoRevno:
742
br_rev_id = branch.last_revision()
747
br_rev_id = branch.last_revision()
750
graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
751
(_mod_revision.NULL_REVISION,))
754
revision_id = next(graph_iter)
755
except errors.RevisionNotPresent as e:
757
yield e.revision_id, None, None
759
except StopIteration:
762
yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
763
if cur_revno is not None:
766
br_rev_id = branch.last_revision()
767
if end_rev_id is None:
768
end_rev_id = br_rev_id
769
found_start = start_rev_id is None
770
graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
771
(_mod_revision.NULL_REVISION,))
774
revision_id = next(graph_iter)
775
except StopIteration:
777
except errors.RevisionNotPresent as e:
779
yield e.revision_id, None, None
782
revno_str = _compute_revno_str(branch, revision_id)
783
if not found_start and revision_id == start_rev_id:
784
if not exclude_common_ancestry:
785
yield revision_id, revno_str, 0
789
yield revision_id, revno_str, 0
791
raise _StartNotLinearAncestor()
794
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
795
rebase_initial_depths=True,
796
exclude_common_ancestry=False):
797
"""Calculate revisions to view including merges, newest to oldest.
799
:param branch: the branch
800
:param start_rev_id: the lower revision-id
801
:param end_rev_id: the upper revision-id
802
:param rebase_initial_depth: should depths be rebased until a mainline
804
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
806
if exclude_common_ancestry:
807
stop_rule = 'with-merges-without-common-ancestry'
809
stop_rule = 'with-merges'
810
view_revisions = branch.iter_merge_sorted_revisions(
811
start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
813
if not rebase_initial_depths:
814
for (rev_id, merge_depth, revno, end_of_merge
816
yield rev_id, '.'.join(map(str, revno)), merge_depth
818
# We're following a development line starting at a merged revision.
819
# We need to adjust depths down by the initial depth until we find
820
# a depth less than it. Then we use that depth as the adjustment.
821
# If and when we reach the mainline, depth adjustment ends.
822
depth_adjustment = None
823
for (rev_id, merge_depth, revno, end_of_merge
825
if depth_adjustment is None:
826
depth_adjustment = merge_depth
828
if merge_depth < depth_adjustment:
829
# From now on we reduce the depth adjustement, this can be
830
# surprising for users. The alternative requires two passes
831
# which breaks the fast display of the first revision
833
depth_adjustment = merge_depth
834
merge_depth -= depth_adjustment
835
yield rev_id, '.'.join(map(str, revno)), merge_depth
838
def _rebase_merge_depth(view_revisions):
839
"""Adjust depths upwards so the top level is 0."""
840
# If either the first or last revision have a merge_depth of 0, we're done
841
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
842
min_depth = min([d for r, n, d in view_revisions])
844
view_revisions = [(r, n, d - min_depth)
845
for r, n, d in view_revisions]
846
return view_revisions
849
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
850
file_ids=None, direction='reverse'):
851
"""Create a revision iterator for log.
853
:param branch: The branch being logged.
854
:param view_revisions: The revisions being viewed.
855
:param generate_delta: Whether to generate a delta for each revision.
856
Permitted values are None, 'full' and 'partial'.
857
:param search: A user text search string.
858
:param file_ids: If non empty, only revisions matching one or more of
859
the file-ids are to be kept.
860
:param direction: the direction in which view_revisions is sorted
861
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
864
# Convert view_revisions into (view, None, None) groups to fit with
865
# the standard interface here.
866
if isinstance(view_revisions, list):
867
# A single batch conversion is faster than many incremental ones.
868
# As we have all the data, do a batch conversion.
869
nones = [None] * len(view_revisions)
870
log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
873
for view in view_revisions:
874
yield (view, None, None)
875
log_rev_iterator = iter([_convert()])
876
for adapter in log_adapters:
877
# It would be nicer if log adapters were first class objects
878
# with custom parameters. This will do for now. IGC 20090127
879
if adapter == _make_delta_filter:
880
log_rev_iterator = adapter(
881
branch, generate_delta, search, log_rev_iterator, file_ids,
884
log_rev_iterator = adapter(
885
branch, generate_delta, search, log_rev_iterator)
886
return log_rev_iterator
889
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
890
"""Create a filtered iterator of log_rev_iterator matching on a regex.
892
:param branch: The branch being logged.
893
:param generate_delta: Whether to generate a delta for each revision.
894
:param match: A dictionary with properties as keys and lists of strings
895
as values. To match, a revision may match any of the supplied strings
896
within a single property but must match at least one string for each
898
:param log_rev_iterator: An input iterator containing all revisions that
899
could be displayed, in lists.
900
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
904
return log_rev_iterator
905
# Use lazy_compile so mapping to InvalidPattern error occurs.
906
searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
907
for k, v in match.items()]
908
return _filter_re(searchRE, log_rev_iterator)
911
def _filter_re(searchRE, log_rev_iterator):
912
for revs in log_rev_iterator:
913
new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
918
def _match_filter(searchRE, rev):
920
'message': (rev.message,),
921
'committer': (rev.committer,),
922
'author': (rev.get_apparent_authors()),
923
'bugs': list(rev.iter_bugs())
925
strings[''] = [item for inner_list in strings.values()
926
for item in inner_list]
927
for k, v in searchRE:
928
if k in strings and not _match_any_filter(strings[k], v):
933
def _match_any_filter(strings, res):
934
return any(r.search(s) for r in res for s in strings)
937
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
938
fileids=None, direction='reverse'):
939
"""Add revision deltas to a log iterator if needed.
941
:param branch: The branch being logged.
942
:param generate_delta: Whether to generate a delta for each revision.
943
Permitted values are None, 'full' and 'partial'.
944
:param search: A user text search string.
945
:param log_rev_iterator: An input iterator containing all revisions that
946
could be displayed, in lists.
947
:param fileids: If non empty, only revisions matching one or more of
948
the file-ids are to be kept.
949
:param direction: the direction in which view_revisions is sorted
950
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
953
if not generate_delta and not fileids:
954
return log_rev_iterator
955
return _generate_deltas(branch.repository, log_rev_iterator,
956
generate_delta, fileids, direction)
959
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
961
"""Create deltas for each batch of revisions in log_rev_iterator.
963
If we're only generating deltas for the sake of filtering against
964
file-ids, we stop generating deltas once all file-ids reach the
965
appropriate life-cycle point. If we're receiving data newest to
966
oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
968
check_fileids = fileids is not None and len(fileids) > 0
970
fileid_set = set(fileids)
971
if direction == 'reverse':
977
for revs in log_rev_iterator:
978
# If we were matching against fileids and we've run out,
979
# there's nothing left to do
980
if check_fileids and not fileid_set:
982
revisions = [rev[1] for rev in revs]
984
if delta_type == 'full' and not check_fileids:
985
deltas = repository.get_deltas_for_revisions(revisions)
986
for rev, delta in zip(revs, deltas):
987
new_revs.append((rev[0], rev[1], delta))
989
deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
990
for rev, delta in zip(revs, deltas):
992
if delta is None or not delta.has_changed():
995
_update_fileids(delta, fileid_set, stop_on)
996
if delta_type is None:
998
elif delta_type == 'full':
999
# If the file matches all the time, rebuilding
1000
# a full delta like this in addition to a partial
1001
# one could be slow. However, it's likely that
1002
# most revisions won't get this far, making it
1003
# faster to filter on the partial deltas and
1004
# build the occasional full delta than always
1005
# building full deltas and filtering those.
1007
delta = repository.get_revision_delta(rev_id)
1008
new_revs.append((rev[0], rev[1], delta))
1012
def _update_fileids(delta, fileids, stop_on):
1013
"""Update the set of file-ids to search based on file lifecycle events.
1015
:param fileids: a set of fileids to update
1016
:param stop_on: either 'add' or 'remove' - take file-ids out of the
1017
fileids set once their add or remove entry is detected respectively
1019
if stop_on == 'add':
1020
for item in delta.added + delta.copied:
1021
if item.file_id in fileids:
1022
fileids.remove(item.file_id)
1023
elif stop_on == 'delete':
1024
for item in delta.removed:
1025
if item.file_id in fileids:
1026
fileids.remove(item.file_id)
1029
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1030
"""Extract revision objects from the repository
1032
:param branch: The branch being logged.
1033
:param generate_delta: Whether to generate a delta for each revision.
1034
:param search: A user text search string.
1035
:param log_rev_iterator: An input iterator containing all revisions that
1036
could be displayed, in lists.
1037
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1040
repository = branch.repository
1041
for revs in log_rev_iterator:
1042
# r = revision_id, n = revno, d = merge depth
1043
revision_ids = [view[0] for view, _, _ in revs]
1044
revisions = dict(repository.iter_revisions(revision_ids))
1045
yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
1048
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
1049
"""Group up a single large batch into smaller ones.
1051
:param branch: The branch being logged.
1052
:param generate_delta: Whether to generate a delta for each revision.
1053
:param search: A user text search string.
1054
:param log_rev_iterator: An input iterator containing all revisions that
1055
could be displayed, in lists.
1056
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1060
for batch in log_rev_iterator:
1063
step = [detail for _, detail in zip(range(num), batch)]
1067
num = min(int(num * 1.5), 200)
1070
def _get_revision_limits(branch, start_revision, end_revision):
1071
"""Get and check revision limits.
1073
:param branch: The branch containing the revisions.
1075
:param start_revision: The first revision to be logged.
1076
but for merge revision support a RevisionInfo is expected.
1078
:param end_revision: The last revision to be logged.
1079
For backwards compatibility this may be a mainline integer revno,
1080
but for merge revision support a RevisionInfo is expected.
1082
:return: (start_rev_id, end_rev_id) tuple.
1086
if start_revision is not None:
1087
if not isinstance(start_revision, revisionspec.RevisionInfo):
1088
raise TypeError(start_revision)
1089
start_rev_id = start_revision.rev_id
1090
start_revno = start_revision.revno
1091
if start_revno is None:
1096
if end_revision is not None:
1097
if not isinstance(end_revision, revisionspec.RevisionInfo):
1098
raise TypeError(start_revision)
1099
end_rev_id = end_revision.rev_id
1100
end_revno = end_revision.revno
1102
if branch.last_revision() != _mod_revision.NULL_REVISION:
1103
if (start_rev_id == _mod_revision.NULL_REVISION
1104
or end_rev_id == _mod_revision.NULL_REVISION):
1105
raise errors.BzrCommandError(
1106
gettext('Logging revision 0 is invalid.'))
1107
if end_revno is not None and start_revno > end_revno:
1108
raise errors.BzrCommandError(
1109
gettext("Start revision must be older than the end revision."))
1110
return (start_rev_id, end_rev_id)
1113
def _get_mainline_revs(branch, start_revision, end_revision):
1114
"""Get the mainline revisions from the branch.
1116
Generates the list of mainline revisions for the branch.
1118
:param branch: The branch containing the revisions.
1120
:param start_revision: The first revision to be logged.
1121
For backwards compatibility this may be a mainline integer revno,
1122
but for merge revision support a RevisionInfo is expected.
1124
:param end_revision: The last revision to be logged.
1125
For backwards compatibility this may be a mainline integer revno,
1126
but for merge revision support a RevisionInfo is expected.
1128
:return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
1130
branch_revno, branch_last_revision = branch.last_revision_info()
1131
if branch_revno == 0:
1132
return None, None, None, None
1134
# For mainline generation, map start_revision and end_revision to
1135
# mainline revnos. If the revision is not on the mainline choose the
1136
# appropriate extreme of the mainline instead - the extra will be
1138
# Also map the revisions to rev_ids, to be used in the later filtering
160
_show_log(branch, lf, specific_fileid, verbose, direction,
161
start_revision, end_revision, search)
165
def _show_log(branch,
167
specific_fileid=None,
173
"""Worker function for show_log - see show_log."""
174
from bzrlib.osutils import format_date
175
from bzrlib.errors import BzrCheckError
176
from bzrlib.textui import show_status
178
from warnings import warn
180
if not isinstance(lf, LogFormatter):
181
warn("not a LogFormatter instance: %r" % lf)
184
mutter('get log for file_id %r', specific_fileid)
186
if search is not None:
188
searchRE = re.compile(search, re.IGNORECASE)
192
which_revs = _enumerate_history(branch)
1141
194
if start_revision is None:
1144
if isinstance(start_revision, revisionspec.RevisionInfo):
1145
start_rev_id = start_revision.rev_id
1146
start_revno = start_revision.revno or 1
1148
branch.check_real_revno(start_revision)
1149
start_revno = start_revision
197
branch.check_real_revno(start_revision)
1152
199
if end_revision is None:
1153
end_revno = branch_revno
1155
if isinstance(end_revision, revisionspec.RevisionInfo):
1156
end_rev_id = end_revision.rev_id
1157
end_revno = end_revision.revno or branch_revno
1159
branch.check_real_revno(end_revision)
1160
end_revno = end_revision
1162
if ((start_rev_id == _mod_revision.NULL_REVISION)
1163
or (end_rev_id == _mod_revision.NULL_REVISION)):
1164
raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1165
if start_revno > end_revno:
1166
raise errors.BzrCommandError(gettext("Start revision must be older "
1167
"than the end revision."))
1169
if end_revno < start_revno:
1170
return None, None, None, None
1171
cur_revno = branch_revno
200
end_revision = len(which_revs)
202
branch.check_real_revno(end_revision)
204
# list indexes are 0-based; revisions are 1-based
205
cut_revs = which_revs[(start_revision-1):(end_revision)]
208
# override the mainline to look like the revision history.
209
mainline_revs = [revision_id for index, revision_id in cut_revs]
210
if cut_revs[0][0] == 1:
211
mainline_revs.insert(0, None)
213
mainline_revs.insert(0, which_revs[start_revision-2][1])
215
merge_sorted_revisions = merge_sort(
216
branch.repository.get_revision_graph(mainline_revs[-1]),
220
if direction == 'reverse':
222
elif direction == 'forward':
223
# forward means oldest first.
224
merge_sorted_revisions.reverse()
226
raise ValueError('invalid direction %r' % direction)
228
revision_history = branch.revision_history()
230
# convert the revision history to a dictionary:
1174
graph = branch.repository.get_graph()
1175
for revision_id in graph.iter_lefthand_ancestry(
1176
branch_last_revision, (_mod_revision.NULL_REVISION,)):
1177
if cur_revno < start_revno:
1178
# We have gone far enough, but we always add 1 more revision
1179
rev_nos[revision_id] = cur_revno
1180
mainline_revs.append(revision_id)
1182
if cur_revno <= end_revno:
1183
rev_nos[revision_id] = cur_revno
1184
mainline_revs.append(revision_id)
1187
# We walked off the edge of all revisions, so we add a 'None' marker
1188
mainline_revs.append(None)
1190
mainline_revs.reverse()
1192
# override the mainline to look like the revision history.
1193
return mainline_revs, rev_nos, start_rev_id, end_rev_id
1196
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1197
include_merges=True):
1198
r"""Return the list of revision ids which touch a given file id.
1200
The function filters view_revisions and returns a subset.
1201
This includes the revisions which directly change the file id,
1202
and the revisions which merge these changes. So if the
1215
And 'C' changes a file, then both C and D will be returned. F will not be
1216
returned even though it brings the changes to C into the branch starting
1217
with E. (Note that if we were using F as the tip instead of G, then we
1220
This will also be restricted based on a subset of the mainline.
1222
:param branch: The branch where we can get text revision information.
1224
:param file_id: Filter out revisions that do not touch file_id.
1226
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1227
tuples. This is the list of revisions which will be filtered. It is
1228
assumed that view_revisions is in merge_sort order (i.e. newest
1231
:param include_merges: include merge revisions in the result or not
1233
:return: A list of (revision_id, dotted_revno, merge_depth) tuples.
1235
# Lookup all possible text keys to determine which ones actually modified
1237
graph = branch.repository.get_file_graph()
1238
get_parent_map = graph.get_parent_map
1239
text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1241
# Looking up keys in batches of 1000 can cut the time in half, as well as
1242
# memory consumption. GraphIndex *does* like to look for a few keys in
1243
# parallel, it just doesn't like looking for *lots* of keys in parallel.
1244
# TODO: This code needs to be re-evaluated periodically as we tune the
1245
# indexing layer. We might consider passing in hints as to the known
1246
# access pattern (sparse/clustered, high success rate/low success
1247
# rate). This particular access is clustered with a low success rate.
1248
modified_text_revisions = set()
1250
for start in range(0, len(text_keys), chunk_size):
1251
next_keys = text_keys[start:start + chunk_size]
1252
# Only keep the revision_id portion of the key
1253
modified_text_revisions.update(
1254
[k[1] for k in get_parent_map(next_keys)])
1255
del text_keys, next_keys
1258
# Track what revisions will merge the current revision, replace entries
1259
# with 'None' when they have been added to result
1260
current_merge_stack = [None]
1261
for info in view_revisions:
1262
rev_id, revno, depth = info
1263
if depth == len(current_merge_stack):
1264
current_merge_stack.append(info)
1266
del current_merge_stack[depth + 1:]
1267
current_merge_stack[-1] = info
1269
if rev_id in modified_text_revisions:
1270
# This needs to be logged, along with the extra revisions
1271
for idx in range(len(current_merge_stack)):
1272
node = current_merge_stack[idx]
1273
if node is not None:
1274
if include_merges or node[2] == 0:
1276
current_merge_stack[idx] = None
1280
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1281
"""Reverse revisions by depth.
1283
Revisions with a different depth are sorted as a group with the previous
1284
revision of that depth. There may be no topological justification for this
1285
but it looks much nicer.
1287
# Add a fake revision at start so that we can always attach sub revisions
1288
merge_sorted_revisions = [(None, None, _depth)] + merge_sorted_revisions
1290
for val in merge_sorted_revisions:
1291
if val[2] == _depth:
1292
# Each revision at the current depth becomes a chunk grouping all
1293
# higher depth revisions.
1294
zd_revisions.append([val])
1296
zd_revisions[-1].append(val)
1297
for revisions in zd_revisions:
1298
if len(revisions) > 1:
1299
# We have higher depth revisions, let reverse them locally
1300
revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
1301
zd_revisions.reverse()
1303
for chunk in zd_revisions:
1304
result.extend(chunk)
1306
# Top level call, get rid of the fake revisions that have been added
1307
result = [r for r in result if r[0] is not None and r[1] is not None]
1311
class LogRevision(object):
1312
"""A revision to be logged (by LogFormatter.log_revision).
1314
A simple wrapper for the attributes of a revision to be logged.
1315
The attributes may or may not be populated, as determined by the
1316
logging options and the log formatter capabilities.
1319
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1320
tags=None, diff=None, signature=None):
1325
self.revno = str(revno)
1326
self.merge_depth = merge_depth
1330
self.signature = signature
232
for index, rev_id in cut_revs:
233
rev_nos[rev_id] = index
235
# now we just print all the revisions
236
for sequence, rev_id, merge_depth, end_of_merge in merge_sorted_revisions:
237
rev = branch.repository.get_revision(rev_id)
240
if not searchRE.search(rev.message):
244
# a mainline revision.
245
if verbose or specific_fileid:
246
delta = _get_revision_delta(branch, rev_nos[rev_id])
249
if not delta.touches_file_id(specific_fileid):
253
# although we calculated it, throw it away without display
256
lf.show(rev_nos[rev_id], rev, delta)
257
elif hasattr(lf, 'show_merge'):
258
lf.show_merge(rev, merge_depth)
261
def deltas_for_log_dummy(branch, which_revs):
262
"""Return all the revisions without intermediate deltas.
264
Useful for log commands that won't need the delta information.
267
for revno, revision_id in which_revs:
268
yield revno, branch.get_revision(revision_id), None
271
def deltas_for_log_reverse(branch, which_revs):
272
"""Compute deltas for display in latest-to-earliest order.
278
Sequence of (revno, revision_id) for the subset of history to examine
281
Sequence of (revno, rev, delta)
283
The delta is from the given revision to the next one in the
284
sequence, which makes sense if the log is being displayed from
287
last_revno = last_revision_id = last_tree = None
288
for revno, revision_id in which_revs:
289
this_tree = branch.revision_tree(revision_id)
290
this_revision = branch.get_revision(revision_id)
293
yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
295
this_tree = EmptyTree(branch.get_root_id())
298
last_revision = this_revision
299
last_tree = this_tree
303
this_tree = EmptyTree(branch.get_root_id())
305
this_revno = last_revno - 1
306
this_revision_id = branch.revision_history()[this_revno]
307
this_tree = branch.revision_tree(this_revision_id)
308
yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
311
def deltas_for_log_forward(branch, which_revs):
312
"""Compute deltas for display in forward log.
314
Given a sequence of (revno, revision_id) pairs, return
317
The delta is from the given revision to the next one in the
318
sequence, which makes sense if the log is being displayed from
321
last_revno = last_revision_id = last_tree = None
322
prev_tree = EmptyTree(branch.get_root_id())
324
for revno, revision_id in which_revs:
325
this_tree = branch.revision_tree(revision_id)
326
this_revision = branch.get_revision(revision_id)
330
last_tree = EmptyTree(branch.get_root_id())
332
last_revno = revno - 1
333
last_revision_id = branch.revision_history()[last_revno]
334
last_tree = branch.revision_tree(last_revision_id)
336
yield revno, this_revision, compare_trees(last_tree, this_tree, False)
339
last_revision = this_revision
340
last_tree = this_tree
1333
343
class LogFormatter(object):
1334
"""Abstract class to display log messages.
1336
At a minimum, a derived class must implement the log_revision method.
1338
If the LogFormatter needs to be informed of the beginning or end of
1339
a log it should implement the begin_log and/or end_log hook methods.
1341
A LogFormatter should define the following supports_XXX flags
1342
to indicate which LogRevision attributes it supports:
1344
- supports_delta must be True if this log formatter supports delta.
1345
Otherwise the delta attribute may not be populated. The 'delta_format'
1346
attribute describes whether the 'short_status' format (1) or the long
1347
one (2) should be used.
1349
- supports_merge_revisions must be True if this log formatter supports
1350
merge revisions. If not, then only mainline revisions will be passed
1353
- preferred_levels is the number of levels this formatter defaults to.
1354
The default value is zero meaning display all levels.
1355
This value is only relevant if supports_merge_revisions is True.
1357
- supports_tags must be True if this log formatter supports tags.
1358
Otherwise the tags attribute may not be populated.
1360
- supports_diff must be True if this log formatter supports diffs.
1361
Otherwise the diff attribute may not be populated.
1363
- supports_signatures must be True if this log formatter supports GPG
1366
Plugins can register functions to show custom revision properties using
1367
the properties_handler_registry. The registered function
1368
must respect the following interface description::
1370
def my_show_properties(properties_dict):
1371
# code that returns a dict {'name':'value'} of the properties
1374
preferred_levels = 0
1376
def __init__(self, to_file, show_ids=False, show_timezone='original',
1377
delta_format=None, levels=None, show_advice=False,
1378
to_exact_file=None, author_list_handler=None):
1379
"""Create a LogFormatter.
1381
:param to_file: the file to output to
1382
:param to_exact_file: if set, gives an output stream to which
1383
non-Unicode diffs are written.
1384
:param show_ids: if True, revision-ids are to be displayed
1385
:param show_timezone: the timezone to use
1386
:param delta_format: the level of delta information to display
1387
or None to leave it to the formatter to decide
1388
:param levels: the number of levels to display; None or -1 to
1389
let the log formatter decide.
1390
:param show_advice: whether to show advice at the end of the
1392
:param author_list_handler: callable generating a list of
1393
authors to display for a given revision
344
"""Abstract class to display log messages."""
345
def __init__(self, to_file, show_ids=False, show_timezone='original'):
1395
346
self.to_file = to_file
1396
# 'exact' stream used to show diff, it should print content 'as is'
1397
# and should not try to decode/encode it to unicode to avoid bug
1399
if to_exact_file is not None:
1400
self.to_exact_file = to_exact_file
1402
# XXX: somewhat hacky; this assumes it's a codec writer; it's
1403
# better for code that expects to get diffs to pass in the exact
1405
self.to_exact_file = getattr(to_file, 'stream', to_file)
1406
347
self.show_ids = show_ids
1407
348
self.show_timezone = show_timezone
1408
if delta_format is None:
1409
# Ensures backward compatibility
1410
delta_format = 2 # long format
1411
self.delta_format = delta_format
1412
self.levels = levels
1413
self._show_advice = show_advice
1414
self._merge_count = 0
1415
self._author_list_handler = author_list_handler
1417
def get_levels(self):
1418
"""Get the number of levels to display or 0 for all."""
1419
if getattr(self, 'supports_merge_revisions', False):
1420
if self.levels is None or self.levels == -1:
1421
self.levels = self.preferred_levels
1426
def log_revision(self, revision):
1429
:param revision: The LogRevision to be logged.
351
def show(self, revno, rev, delta):
1431
352
raise NotImplementedError('not implemented in abstract base')
1433
def show_advice(self):
1434
"""Output user advice, if any, when the log is completed."""
1435
if self._show_advice and self.levels == 1 and self._merge_count > 0:
1436
advice_sep = self.get_advice_separator()
1438
self.to_file.write(advice_sep)
1440
"Use --include-merged or -n0 to see merged revisions.\n")
1442
def get_advice_separator(self):
1443
"""Get the text separating the log from the closing advice."""
1446
354
def short_committer(self, rev):
1447
name, address = config.parse_username(rev.committer)
1452
def short_author(self, rev):
1453
return self.authors(rev, 'first', short=True, sep=', ')
1455
def authors(self, rev, who, short=False, sep=None):
1456
"""Generate list of authors, taking --authors option into account.
1458
The caller has to specify the name of a author list handler,
1459
as provided by the author list registry, using the ``who``
1460
argument. That name only sets a default, though: when the
1461
user selected a different author list generation using the
1462
``--authors`` command line switch, as represented by the
1463
``author_list_handler`` constructor argument, that value takes
1466
:param rev: The revision for which to generate the list of authors.
1467
:param who: Name of the default handler.
1468
:param short: Whether to shorten names to either name or address.
1469
:param sep: What separator to use for automatic concatenation.
1471
if self._author_list_handler is not None:
1472
# The user did specify --authors, which overrides the default
1473
author_list_handler = self._author_list_handler
1475
# The user didn't specify --authors, so we use the caller's default
1476
author_list_handler = author_list_registry.get(who)
1477
names = author_list_handler(rev)
1479
for i in range(len(names)):
1480
name, address = config.parse_username(names[i])
1486
names = sep.join(names)
1489
def merge_marker(self, revision):
1490
"""Get the merge marker to include in the output or '' if none."""
1491
if len(revision.rev.parent_ids) > 1:
1492
self._merge_count += 1
1497
def show_properties(self, revision, indent):
1498
"""Displays the custom properties returned by each registered handler.
1500
If a registered handler raises an error it is propagated.
1502
for line in self.custom_properties(revision):
1503
self.to_file.write("%s%s\n" % (indent, line))
1505
def custom_properties(self, revision):
1506
"""Format the custom properties returned by each registered handler.
1508
If a registered handler raises an error it is propagated.
1510
:return: a list of formatted lines (excluding trailing newlines)
1512
lines = self._foreign_info_properties(revision)
1513
for key, handler in properties_handler_registry.iteritems():
1515
lines.extend(self._format_properties(handler(revision)))
1517
trace.log_exception_quietly()
1518
trace.print_exception(sys.exc_info(), self.to_file)
1521
def _foreign_info_properties(self, rev):
1522
"""Custom log displayer for foreign revision identifiers.
1524
:param rev: Revision object.
1526
# Revision comes directly from a foreign repository
1527
if isinstance(rev, foreign.ForeignRevision):
1528
return self._format_properties(
1529
rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1531
# Imported foreign revision revision ids always contain :
1532
if b":" not in rev.revision_id:
1535
# Revision was once imported from a foreign repository
1537
foreign_revid, mapping = \
1538
foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1539
except errors.InvalidRevisionId:
1542
return self._format_properties(
1543
mapping.vcs.show_foreign_revid(foreign_revid))
1545
def _format_properties(self, properties):
1547
for key, value in properties.items():
1548
lines.append(key + ': ' + value)
1551
def show_diff(self, to_file, diff, indent):
1552
encoding = get_terminal_encoding()
1553
for l in diff.rstrip().split(b'\n'):
1554
to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
1557
# Separator between revisions in long format
1558
_LONG_SEP = '-' * 60
355
return re.sub('<.*@.*>', '', rev.committer).strip(' ')
1561
358
class LongLogFormatter(LogFormatter):
1563
supports_merge_revisions = True
1564
preferred_levels = 1
1565
supports_delta = True
1566
supports_tags = True
1567
supports_diff = True
1568
supports_signatures = True
1570
def __init__(self, *args, **kwargs):
1571
super(LongLogFormatter, self).__init__(*args, **kwargs)
1572
if self.show_timezone == 'original':
1573
self.date_string = self._date_string_original_timezone
1575
self.date_string = self._date_string_with_timezone
1577
def _date_string_with_timezone(self, rev):
1578
return format_date(rev.timestamp, rev.timezone or 0,
1581
def _date_string_original_timezone(self, rev):
1582
return format_date_with_offset_in_original_timezone(rev.timestamp,
1585
def log_revision(self, revision):
1586
"""Log a revision, either merged or not."""
1587
indent = ' ' * revision.merge_depth
1589
if revision.revno is not None:
1590
lines.append('revno: %s%s' % (revision.revno,
1591
self.merge_marker(revision)))
1593
lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
1594
if self.show_ids or revision.revno is None:
1595
lines.append('revision-id: %s' %
1596
(revision.rev.revision_id.decode('utf-8'),))
359
def show(self, revno, rev, delta):
360
return self._show_helper(revno=revno, rev=rev, delta=delta)
362
def show_merge(self, rev, merge_depth):
363
return self._show_helper(rev=rev, indent=' '*merge_depth, merged=True, delta=None)
365
def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
366
"""Show a revision, either merged or not."""
367
from bzrlib.osutils import format_date
368
to_file = self.to_file
369
print >>to_file, indent+'-' * 60
370
if revno is not None:
371
print >>to_file, 'revno:', revno
373
print >>to_file, indent+'merged:', rev.revision_id
375
print >>to_file, indent+'revision-id:', rev.revision_id
1597
376
if self.show_ids:
1598
for parent_id in revision.rev.parent_ids:
1599
lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1600
lines.extend(self.custom_properties(revision.rev))
1602
committer = revision.rev.committer
1603
authors = self.authors(revision.rev, 'all')
1604
if authors != [committer]:
1605
lines.append('author: %s' % (", ".join(authors),))
1606
lines.append('committer: %s' % (committer,))
1608
branch_nick = revision.rev.properties.get('branch-nick', None)
1609
if branch_nick is not None:
1610
lines.append('branch nick: %s' % (branch_nick,))
1612
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1614
if revision.signature is not None:
1615
lines.append('signature: ' + revision.signature)
1617
lines.append('message:')
1618
if not revision.rev.message:
1619
lines.append(' (no message)')
377
for parent_id in rev.parent_ids:
378
print >>to_file, indent+'parent:', parent_id
379
print >>to_file, indent+'committer:', rev.committer
381
print >>to_file, indent+'branch nick: %s' % \
382
rev.properties['branch-nick']
385
date_str = format_date(rev.timestamp,
388
print >>to_file, indent+'timestamp: %s' % date_str
390
print >>to_file, indent+'message:'
392
print >>to_file, indent+' (no message)'
1621
message = revision.rev.message.rstrip('\r\n')
394
message = rev.message.rstrip('\r\n')
1622
395
for l in message.split('\n'):
1623
lines.append(' %s' % (l,))
1625
# Dump the output, appending the delta and diff if requested
1626
to_file = self.to_file
1627
to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1628
if revision.delta is not None:
1629
# Use the standard status output to display changes
1630
from breezy.delta import report_delta
1631
report_delta(to_file, revision.delta, short_status=False,
1632
show_ids=self.show_ids, indent=indent)
1633
if revision.diff is not None:
1634
to_file.write(indent + 'diff:\n')
1636
# Note: we explicitly don't indent the diff (relative to the
1637
# revision information) so that the output can be fed to patch -p0
1638
self.show_diff(self.to_exact_file, revision.diff, indent)
1639
self.to_exact_file.flush()
1641
def get_advice_separator(self):
1642
"""Get the text separating the log from the closing advice."""
1643
return '-' * 60 + '\n'
396
print >>to_file, indent+' ' + l
398
delta.show(to_file, self.show_ids)
1646
401
class ShortLogFormatter(LogFormatter):
1648
supports_merge_revisions = True
1649
preferred_levels = 1
1650
supports_delta = True
1651
supports_tags = True
1652
supports_diff = True
1654
def __init__(self, *args, **kwargs):
1655
super(ShortLogFormatter, self).__init__(*args, **kwargs)
1656
self.revno_width_by_depth = {}
1658
def log_revision(self, revision):
1659
# We need two indents: one per depth and one for the information
1660
# relative to that indent. Most mainline revnos are 5 chars or
1661
# less while dotted revnos are typically 11 chars or less. Once
1662
# calculated, we need to remember the offset for a given depth
1663
# as we might be starting from a dotted revno in the first column
1664
# and we want subsequent mainline revisions to line up.
1665
depth = revision.merge_depth
1666
indent = ' ' * depth
1667
revno_width = self.revno_width_by_depth.get(depth)
1668
if revno_width is None:
1669
if revision.revno is None or revision.revno.find('.') == -1:
1670
# mainline revno, e.g. 12345
1673
# dotted revno, e.g. 12345.10.55
1675
self.revno_width_by_depth[depth] = revno_width
1676
offset = ' ' * (revno_width + 1)
402
def show(self, revno, rev, delta):
403
from bzrlib.osutils import format_date
1678
405
to_file = self.to_file
1681
tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1682
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1683
revision.revno or "", self.short_author(
1685
format_date(revision.rev.timestamp,
1686
revision.rev.timezone or 0,
1687
self.show_timezone, date_fmt="%Y-%m-%d",
1689
tags, self.merge_marker(revision)))
1690
self.show_properties(revision.rev, indent + offset)
1691
if self.show_ids or revision.revno is None:
1692
to_file.write(indent + offset + 'revision-id:%s\n'
1693
% (revision.rev.revision_id.decode('utf-8'),))
1694
if not revision.rev.message:
1695
to_file.write(indent + offset + '(no message)\n')
406
date_str = format_date(rev.timestamp, rev.timezone or 0,
408
print >>to_file, "%5d %s\t%s" % (revno, self.short_committer(rev),
409
format_date(rev.timestamp, rev.timezone or 0,
410
self.show_timezone, date_fmt="%Y-%m-%d",
413
print >>to_file, ' revision-id:', rev.revision_id
415
print >>to_file, ' (no message)'
1697
message = revision.rev.message.rstrip('\r\n')
417
message = rev.message.rstrip('\r\n')
1698
418
for l in message.split('\n'):
1699
to_file.write(indent + offset + '%s\n' % (l,))
1701
if revision.delta is not None:
1702
# Use the standard status output to display changes
1703
from breezy.delta import report_delta
1704
report_delta(to_file, revision.delta,
1705
short_status=self.delta_format == 1,
1706
show_ids=self.show_ids, indent=indent + offset)
1707
if revision.diff is not None:
1708
self.show_diff(self.to_exact_file, revision.diff, ' ')
419
print >>to_file, ' ' + l
421
# TODO: Why not show the modified files in a shorter form as
422
# well? rewrap them single lines of appropriate length
424
delta.show(to_file, self.show_ids)
1712
427
class LineLogFormatter(LogFormatter):
1714
supports_merge_revisions = True
1715
preferred_levels = 1
1716
supports_tags = True
1718
def __init__(self, *args, **kwargs):
1719
super(LineLogFormatter, self).__init__(*args, **kwargs)
1720
width = terminal_width()
1721
if width is not None:
1722
# we need one extra space for terminals that wrap on last char
1724
self._max_chars = width
1726
428
def truncate(self, str, max_len):
1727
if max_len is None or len(str) <= max_len:
429
if len(str) <= max_len:
1729
return str[:max_len - 3] + '...'
431
return str[:max_len-3]+'...'
1731
433
def date_string(self, rev):
1732
return format_date(rev.timestamp, rev.timezone or 0,
434
from bzrlib.osutils import format_date
435
return format_date(rev.timestamp, rev.timezone or 0,
1733
436
self.show_timezone, date_fmt="%Y-%m-%d",
1734
437
show_offset=False)