47
49
all the changes since the previous revision that touched hello.c.
50
from __future__ import absolute_import
53
from cStringIO import StringIO
54
from itertools import (
56
60
from warnings import (
60
from .lazy_import import lazy_import
64
from bzrlib.lazy_import import lazy_import
61
65
lazy_import(globals(), """
73
repository as _mod_repository,
69
74
revision as _mod_revision,
71
from breezy.i18n import gettext, ngettext
80
from .osutils import (
84
from bzrlib.osutils import (
82
86
format_date_with_offset_in_original_timezone,
83
get_diff_header_encoding,
84
87
get_terminal_encoding,
91
from bzrlib.symbol_versioning import (
92
from .tree import InterTree
95
def find_touching_revisions(repository, last_revision, last_tree, last_path):
97
def find_touching_revisions(branch, file_id):
96
98
"""Yield a description of revisions which affect the file_id.
98
100
Each returned element is (revno, revision_id, description)
103
105
TODO: Perhaps some way to limit this to only particular revisions,
104
106
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)
111
for revision_id in branch.revision_history():
112
this_inv = branch.repository.get_revision_inventory(revision_id)
113
if file_id in this_inv:
114
this_ie = this_inv[file_id]
115
this_path = this_inv.id2path(file_id)
117
this_ie = this_path = None
115
119
# now we know how it was last time, and how it is in this revision.
116
120
# 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
122
if not this_ie and not last_ie:
123
# not present in either
125
elif this_ie and not last_ie:
126
yield revno, revision_id, "added " + this_path
127
elif not this_ie and last_ie:
129
yield revno, revision_id, "deleted " + last_path
122
130
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
131
yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
132
elif (this_ie.text_size != last_ie.text_size
133
or this_ie.text_sha1 != last_ie.text_sha1):
134
yield revno, revision_id, "modified " + this_path
130
last_verifier = this_verifier
131
137
last_path = this_path
132
last_tree = this_tree
133
if last_path is None:
141
def _enumerate_history(branch):
144
for rev_id in branch.revision_history():
145
rh.append((revno, rev_id))
138
150
def show_log(branch,
152
specific_fileid=None,
141
154
direction='reverse',
142
155
start_revision=None,
143
156
end_revision=None,
148
160
"""Write out human-readable log of commits to this branch.
150
162
This function is being retained for backwards compatibility but
172
187
:param show_diff: If True, output a diff after each revision.
174
:param match: Dictionary of search lists to use when matching revision
189
# Convert old-style parameters to new-style parameters
190
if specific_fileid is not None:
191
file_ids = [specific_fileid]
196
delta_type = 'partial'
180
200
delta_type = None
203
diff_type = 'partial'
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
209
# Build the request and execute it
202
rqst = make_log_request_dict(
210
rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
204
211
start_revision=start_revision, end_revision=end_revision,
205
212
limit=limit, message_search=search,
206
213
delta_type=delta_type, diff_type=diff_type)
207
214
Logger(branch, rqst).show(lf)
210
# Note: This needs to be kept in sync with the defaults in
217
# Note: This needs to be kept this in sync with the defaults in
211
218
# make_log_request_dict() below
212
219
_DEFAULT_REQUEST_PARAMS = {
213
220
'direction': 'reverse',
215
222
'generate_tags': True,
216
'exclude_common_ancestry': False,
217
223
'_match_using_deltas': True,
221
227
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,
228
start_revision=None, end_revision=None, limit=None,
229
message_search=None, levels=1, generate_tags=True, delta_type=None,
230
diff_type=None, _match_using_deltas=True):
229
231
"""Convenience function for making a logging request dictionary.
231
233
Using this function may make code slightly safer by ensuring
269
270
:param _match_using_deltas: a private parameter controlling the
270
271
algorithm used for matching specific_fileids. This parameter
271
may be removed in the future so breezy client code should NOT
272
may be removed in the future so bzrlib 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
276
'direction': direction,
298
277
'specific_fileids': specific_fileids,
299
278
'start_revision': start_revision,
300
279
'end_revision': end_revision,
281
'message_search': message_search,
302
282
'levels': levels,
303
283
'generate_tags': generate_tags,
304
284
'delta_type': delta_type,
305
285
'diff_type': diff_type,
306
'exclude_common_ancestry': exclude_common_ancestry,
307
'signature': signature,
309
'omit_merges': omit_merges,
310
286
# Add 'private' attributes for features that may be deprecated
311
287
'_match_using_deltas': _match_using_deltas,
388
346
# Tweak the LogRequest based on what the LogFormatter can handle.
389
347
# (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()
349
rqst['levels'] = lf.get_levels()
395
350
if not getattr(lf, 'supports_tags', False):
396
351
rqst['generate_tags'] = False
397
352
if not getattr(lf, 'supports_delta', False):
398
353
rqst['delta_type'] = None
399
354
if not getattr(lf, 'supports_diff', False):
400
355
rqst['diff_type'] = None
401
if not getattr(lf, 'supports_signatures', False):
402
rqst['signature'] = False
404
357
# Find and print the interesting revisions
405
358
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.'))
359
for lr in generator.iter_log_revisions():
414
363
def _generator_factory(self, branch, rqst):
415
364
"""Make the LogGenerator object to use.
417
366
Subclasses may wish to override this.
419
368
return _DefaultLogGenerator(branch, rqst)
443
392
levels = rqst.get('levels')
444
393
limit = rqst.get('limit')
445
394
diff_type = rqst.get('diff_type')
446
show_signature = rqst.get('signature')
447
omit_merges = rqst.get('omit_merges')
449
396
revision_iterator = self._create_log_revision_iterator()
450
397
for revs in revision_iterator:
451
398
for (rev_id, revno, merge_depth), rev, delta in revs:
452
399
# 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)
400
if levels != 0 and merge_depth >= levels:
460
402
if diff_type is None:
463
405
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)
406
yield LogRevision(rev, revno, merge_depth, delta,
407
self.rev_tag_dict.get(rev_id), diff)
473
410
if log_count >= limit:
520
455
generate_merge_revisions = rqst.get('levels') != 1
521
456
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'))
457
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
458
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
459
self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
460
delayed_graph_generation=delayed_graph_generation)
530
462
# Apply the other filters
531
463
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'))
464
rqst.get('delta_type'), rqst.get('message_search'),
465
file_ids=rqst.get('specific_fileids'),
466
direction=rqst.get('direction'))
536
468
def _log_revision_iterator_using_per_file_graph(self):
537
469
# Get the base revisions, filtering by the revision range.
538
470
# Note that we always generate the merge revisions because
539
471
# 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'))
473
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
474
self.end_rev_id, rqst.get('direction'), True)
545
475
if not isinstance(view_revisions, list):
546
476
view_revisions = list(view_revisions)
547
477
view_revisions = _filter_revisions_touching_file_id(self.branch,
548
rqst.get('specific_fileids')[
550
include_merges=rqst.get('levels') != 1)
478
rqst.get('specific_fileids')[0], view_revisions,
479
include_merges=rqst.get('levels') != 1)
551
480
return make_log_rev_iterator(self.branch, view_revisions,
552
rqst.get('delta_type'), rqst.get('match'))
481
rqst.get('delta_type'), rqst.get('message_search'))
555
484
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,
485
generate_merge_revisions, delayed_graph_generation=False):
560
486
"""Calculate the revisions to view.
562
488
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
563
489
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:
491
br_revno, br_rev_id = branch.last_revision_info()
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,
495
# If a single revision is requested, check we can handle it
496
generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
497
(not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
498
if generate_single_revision:
499
return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
501
# If we only want to see linear revisions, we can iterate ...
580
502
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)))
503
return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
506
return _generate_all_revisions(branch, start_rev_id, end_rev_id,
507
direction, delayed_graph_generation)
607
510
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
610
513
return [(br_rev_id, br_revno, 0)]
612
revno_str = _compute_revno_str(branch, rev_id)
515
revno = branch.revision_id_to_dotted_revno(rev_id)
516
revno_str = '.'.join(str(n) for n in revno)
613
517
return [(rev_id, revno_str, 0)]
520
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
521
result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
522
# If a start limit was given and it's not obviously an
523
# ancestor of the end limit, check it before outputting anything
524
if direction == 'forward' or (start_rev_id
525
and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
527
result = list(result)
528
except _StartNotLinearAncestor:
529
raise errors.BzrCommandError('Start revision not found in'
530
' left-hand history of end revision.')
531
if direction == 'forward':
532
result = reversed(result)
616
536
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
617
delayed_graph_generation,
618
exclude_common_ancestry=False):
537
delayed_graph_generation):
619
538
# On large trees, generating the merge graph can take 30-60 seconds
620
539
# so we delay doing it until a merge is detected, incrementally
621
540
# returning initial (non-merge) revisions while we can.
626
545
initial_revisions = []
627
546
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):
548
for rev_id, revno, depth in _linear_view_revisions(
549
branch, start_rev_id, end_rev_id):
631
550
if _has_merges(branch, rev_id):
632
551
# The end_rev_id can be nested down somewhere. We need an
633
552
# explicit ancestry check. There is an ambiguity here as we
634
553
# may not raise _StartNotLinearAncestor for a revision that
635
554
# is an ancestor but not a *linear* one. But since we have
636
555
# 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
556
# revno), we may as well accept to show the log...
641
558
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)):
559
if not graph.is_ancestor(start_rev_id, end_rev_id):
644
560
raise _StartNotLinearAncestor()
645
# Since we collected the revisions so far, we need to
647
561
end_rev_id = rev_id
650
564
initial_revisions.append((rev_id, revno, depth))
652
566
# No merged revisions found
653
return initial_revisions
567
if direction == 'reverse':
568
return initial_revisions
569
elif direction == 'forward':
570
return reversed(initial_revisions)
572
raise ValueError('invalid direction %r' % direction)
654
573
except _StartNotLinearAncestor:
655
574
# A merge was never detected so the lower revision limit can't
656
575
# 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.
576
raise errors.BzrCommandError('Start revision not found in'
577
' history of end revision.')
663
579
# A log including nested merges is required. If the direction is reverse,
664
580
# we rebase the initial merge depths so that the development line is
665
581
# shown naturally, i.e. just like it is for linear logging. We can easily
666
582
# make forward the exact opposite display, but showing the merge revisions
667
583
# 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
584
view_revisions = chain(iter(initial_revisions),
585
_graph_view_revisions(branch, start_rev_id, end_rev_id,
586
rebase_initial_depths=direction == 'reverse'))
587
if direction == 'reverse':
588
return view_revisions
589
elif direction == 'forward':
590
# Forward means oldest first, adjusting for depth.
591
view_revisions = reverse_by_depth(list(view_revisions))
592
return _rebase_merge_depth(view_revisions)
594
raise ValueError('invalid direction %r' % direction)
676
597
def _has_merges(branch, rev_id):
679
600
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
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
603
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
698
604
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
699
605
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
606
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
607
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
706
608
if len(start_dotted) == 1 and len(end_dotted) == 1:
707
609
# both on mainline
708
610
return start_dotted[0] <= end_dotted[0]
709
611
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
710
start_dotted[0:1] == end_dotted[0:1]):
612
start_dotted[0:1] == end_dotted[0:1]):
711
613
# both on same development line
712
614
return start_dotted[2] <= end_dotted[2]
721
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
722
exclude_common_ancestry=False):
623
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
723
624
"""Calculate a sequence of revisions to view, newest to oldest.
725
626
:param start_rev_id: the lower revision-id
726
627
: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
628
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
730
dotted_revno will be None for ghosts
731
629
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
732
is not found walking the left-hand history
630
is not found walking the left-hand history
632
br_revno, br_rev_id = branch.last_revision_info()
734
633
repo = branch.repository
735
graph = repo.get_graph()
736
634
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:
636
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
637
yield revision_id, str(cur_revno), 0
766
br_rev_id = branch.last_revision()
767
640
if end_rev_id is None:
768
641
end_rev_id = br_rev_id
769
642
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
643
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
644
revno = branch.revision_id_to_dotted_revno(revision_id)
645
revno_str = '.'.join(str(n) for n in revno)
646
if not found_start and revision_id == start_rev_id:
647
yield revision_id, revno_str, 0
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()
651
yield revision_id, revno_str, 0
654
raise _StartNotLinearAncestor()
794
657
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
795
rebase_initial_depths=True,
796
exclude_common_ancestry=False):
658
rebase_initial_depths=True):
797
659
"""Calculate revisions to view including merges, newest to oldest.
799
661
:param branch: the branch
835
693
yield rev_id, '.'.join(map(str, revno)), merge_depth
696
@deprecated_function(deprecated_in((2, 2, 0)))
697
def calculate_view_revisions(branch, start_revision, end_revision, direction,
698
specific_fileid, generate_merge_revisions):
699
"""Calculate the revisions to view.
701
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
702
a list of the same tuples.
704
start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
706
view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
707
direction, generate_merge_revisions or specific_fileid))
709
view_revisions = _filter_revisions_touching_file_id(branch,
710
specific_fileid, view_revisions,
711
include_merges=generate_merge_revisions)
712
return _rebase_merge_depth(view_revisions)
838
715
def _rebase_merge_depth(view_revisions):
839
716
"""Adjust depths upwards so the top level is 0."""
840
717
# If either the first or last revision have a merge_depth of 0, we're done
841
718
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])
719
min_depth = min([d for r,n,d in view_revisions])
843
720
if min_depth != 0:
844
view_revisions = [(r, n, d - min_depth)
845
for r, n, d in view_revisions]
721
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
846
722
return view_revisions
849
725
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
850
file_ids=None, direction='reverse'):
726
file_ids=None, direction='reverse'):
851
727
"""Create a revision iterator for log.
853
729
:param branch: The branch being logged.
877
753
# It would be nicer if log adapters were first class objects
878
754
# with custom parameters. This will do for now. IGC 20090127
879
755
if adapter == _make_delta_filter:
880
log_rev_iterator = adapter(
881
branch, generate_delta, search, log_rev_iterator, file_ids,
756
log_rev_iterator = adapter(branch, generate_delta,
757
search, log_rev_iterator, file_ids, direction)
884
log_rev_iterator = adapter(
885
branch, generate_delta, search, log_rev_iterator)
759
log_rev_iterator = adapter(branch, generate_delta,
760
search, log_rev_iterator)
886
761
return log_rev_iterator
889
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
764
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
890
765
"""Create a filtered iterator of log_rev_iterator matching on a regex.
892
767
:param branch: The branch being logged.
893
768
: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
769
:param search: A user text search string.
898
770
:param log_rev_iterator: An input iterator containing all revisions that
899
771
could be displayed, in lists.
900
772
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
904
776
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):
777
searchRE = re_compile_checked(search, re.IGNORECASE,
778
'log message filter')
779
return _filter_message_re(searchRE, log_rev_iterator)
782
def _filter_message_re(searchRE, log_rev_iterator):
912
783
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)
785
for (rev_id, revno, merge_depth), rev, delta in revs:
786
if searchRE.search(rev.message):
787
new_revs.append(((rev_id, revno, merge_depth), rev, delta))
937
791
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
938
fileids=None, direction='reverse'):
792
fileids=None, direction='reverse'):
939
793
"""Add revision deltas to a log iterator if needed.
941
795
:param branch: The branch being logged.
1082
940
:return: (start_rev_id, end_rev_id) tuple.
942
branch_revno, branch_rev_id = branch.last_revision_info()
1084
943
start_rev_id = None
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:
944
if start_revision is None:
947
if isinstance(start_revision, revisionspec.RevisionInfo):
948
start_rev_id = start_revision.rev_id
949
start_revno = start_revision.revno or 1
951
branch.check_real_revno(start_revision)
952
start_revno = start_revision
953
start_rev_id = branch.get_rev_id(start_revno)
1094
955
end_rev_id = 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
956
if end_revision is None:
957
end_revno = branch_revno
959
if isinstance(end_revision, revisionspec.RevisionInfo):
960
end_rev_id = end_revision.rev_id
961
end_revno = end_revision.revno or branch_revno
963
branch.check_real_revno(end_revision)
964
end_revno = end_revision
965
end_rev_id = branch.get_rev_id(end_revno)
1102
if branch.last_revision() != _mod_revision.NULL_REVISION:
967
if branch_revno != 0:
1103
968
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."))
969
or end_rev_id == _mod_revision.NULL_REVISION):
970
raise errors.BzrCommandError('Logging revision 0 is invalid.')
971
if start_revno > end_revno:
972
raise errors.BzrCommandError("Start revision must be older than "
1110
974
return (start_rev_id, end_rev_id)
1193
1056
return mainline_revs, rev_nos, start_rev_id, end_rev_id
1059
@deprecated_function(deprecated_in((2, 2, 0)))
1060
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1061
"""Filter view_revisions based on revision ranges.
1063
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1064
tuples to be filtered.
1066
:param start_rev_id: If not NONE specifies the first revision to be logged.
1067
If NONE then all revisions up to the end_rev_id are logged.
1069
:param end_rev_id: If not NONE specifies the last revision to be logged.
1070
If NONE then all revisions up to the end of the log are logged.
1072
:return: The filtered view_revisions.
1074
if start_rev_id or end_rev_id:
1075
revision_ids = [r for r, n, d in view_revisions]
1077
start_index = revision_ids.index(start_rev_id)
1080
if start_rev_id == end_rev_id:
1081
end_index = start_index
1084
end_index = revision_ids.index(end_rev_id)
1086
end_index = len(view_revisions) - 1
1087
# To include the revisions merged into the last revision,
1088
# extend end_rev_id down to, but not including, the next rev
1089
# with the same or lesser merge_depth
1090
end_merge_depth = view_revisions[end_index][2]
1092
for index in xrange(end_index+1, len(view_revisions)+1):
1093
if view_revisions[index][2] <= end_merge_depth:
1094
end_index = index - 1
1097
# if the search falls off the end then log to the end as well
1098
end_index = len(view_revisions) - 1
1099
view_revisions = view_revisions[start_index:end_index+1]
1100
return view_revisions
1196
1103
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1197
include_merges=True):
1104
include_merges=True):
1198
1105
r"""Return the list of revision ids which touch a given file id.
1200
1107
The function filters view_revisions and returns a subset.
1201
1108
This includes the revisions which directly change the file id,
1202
1109
and the revisions which merge these changes. So if the
1203
1110
revision graph is::
1185
@deprecated_function(deprecated_in((2, 2, 0)))
1186
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1187
include_merges=True):
1188
"""Produce an iterator of revisions to show
1189
:return: an iterator of (revision_id, revno, merge_depth)
1190
(if there is no revno for a revision, None is supplied)
1192
if not include_merges:
1193
revision_ids = mainline_revs[1:]
1194
if direction == 'reverse':
1195
revision_ids.reverse()
1196
for revision_id in revision_ids:
1197
yield revision_id, str(rev_nos[revision_id]), 0
1199
graph = branch.repository.get_graph()
1200
# This asks for all mainline revisions, which means we only have to spider
1201
# sideways, rather than depth history. That said, its still size-of-history
1202
# and should be addressed.
1203
# mainline_revisions always includes an extra revision at the beginning, so
1205
parent_map = dict(((key, value) for key, value in
1206
graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1207
# filter out ghosts; merge_sort errors on ghosts.
1208
rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1209
merge_sorted_revisions = tsort.merge_sort(
1213
generate_revno=True)
1215
if direction == 'forward':
1216
# forward means oldest first.
1217
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1218
elif direction != 'reverse':
1219
raise ValueError('invalid direction %r' % direction)
1221
for (sequence, rev_id, merge_depth, revno, end_of_merge
1222
) in merge_sorted_revisions:
1223
yield rev_id, '.'.join(map(str, revno)), merge_depth
1280
1226
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1281
1227
"""Reverse revisions by depth.
1283
1229
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
1230
revision of that depth. There may be no topological justification for this,
1285
1231
but it looks much nicer.
1287
1233
# Add a fake revision at start so that we can always attach sub revisions
1342
1284
to indicate which LogRevision attributes it supports:
1344
1286
- 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.
1287
Otherwise the delta attribute may not be populated. The 'delta_format'
1288
attribute describes whether the 'short_status' format (1) or the long
1289
one (2) should be used.
1349
1291
- supports_merge_revisions must be True if this log formatter supports
1350
merge revisions. If not, then only mainline revisions will be passed
1292
merge revisions. If not, then only mainline revisions will be passed
1353
1295
- 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.
1296
The default value is zero meaning display all levels.
1297
This value is only relevant if supports_merge_revisions is True.
1357
1299
- supports_tags must be True if this log formatter supports tags.
1358
Otherwise the tags attribute may not be populated.
1300
Otherwise the tags attribute may not be populated.
1360
1302
- 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
1303
Otherwise the diff attribute may not be populated.
1366
1305
Plugins can register functions to show custom revision properties using
1367
1306
the properties_handler_registry. The registered function
1368
must respect the following interface description::
1307
must respect the following interface description:
1370
1308
def my_show_properties(properties_dict):
1371
1309
# code that returns a dict {'name':'value'} of the properties
1389
1327
let the log formatter decide.
1390
1328
: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
1395
1331
self.to_file = to_file
1396
1332
# '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
1333
# and should not try to decode/encode it to unicode to avoid bug #328007
1399
1334
if to_exact_file is not None:
1400
1335
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
1337
# XXX: somewhat hacky; this assumes it's a codec writer; it's better
1338
# for code that expects to get diffs to pass in the exact file
1405
1340
self.to_exact_file = getattr(to_file, 'stream', to_file)
1406
1341
self.show_ids = show_ids
1407
1342
self.show_timezone = show_timezone
1408
1343
if delta_format is None:
1409
1344
# Ensures backward compatibility
1410
delta_format = 2 # long format
1345
delta_format = 2 # long format
1411
1346
self.delta_format = delta_format
1412
1347
self.levels = levels
1413
1348
self._show_advice = show_advice
1414
1349
self._merge_count = 0
1415
self._author_list_handler = author_list_handler
1417
1351
def get_levels(self):
1418
1352
"""Get the number of levels to display or 0 for all."""
1452
1386
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)
1387
name, address = config.parse_username(rev.get_apparent_authors()[0])
1489
1392
def merge_marker(self, revision):
1490
1393
"""Get the merge marker to include in the output or '' if none."""
1588
1484
lines = [_LONG_SEP]
1589
1485
if revision.revno is not None:
1590
1486
lines.append('revno: %s%s' % (revision.revno,
1591
self.merge_marker(revision)))
1487
self.merge_marker(revision)))
1592
1488
if revision.tags:
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'),))
1489
lines.append('tags: %s' % (', '.join(revision.tags)))
1597
1490
if self.show_ids:
1491
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1598
1492
for parent_id in revision.rev.parent_ids:
1599
lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1493
lines.append('parent: %s' % (parent_id,))
1600
1494
lines.extend(self.custom_properties(revision.rev))
1602
1496
committer = revision.rev.committer
1603
authors = self.authors(revision.rev, 'all')
1497
authors = revision.rev.get_apparent_authors()
1604
1498
if authors != [committer]:
1605
1499
lines.append('author: %s' % (", ".join(authors),))
1606
1500
lines.append('committer: %s' % (committer,))
1678
1568
to_file = self.to_file
1680
1570
if revision.tags:
1681
tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1571
tags = ' {%s}' % (', '.join(revision.tags))
1682
1572
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:
1573
revision.revno, self.short_author(revision.rev),
1574
format_date(revision.rev.timestamp,
1575
revision.rev.timezone or 0,
1576
self.show_timezone, date_fmt="%Y-%m-%d",
1578
tags, self.merge_marker(revision)))
1579
self.show_properties(revision.rev, indent+offset)
1692
1581
to_file.write(indent + offset + 'revision-id:%s\n'
1693
% (revision.rev.revision_id.decode('utf-8'),))
1582
% (revision.rev.revision_id,))
1694
1583
if not revision.rev.message:
1695
1584
to_file.write(indent + offset + '(no message)\n')
1742
1628
def log_revision(self, revision):
1743
1629
indent = ' ' * revision.merge_depth
1744
1630
self.to_file.write(self.log_string(revision.revno, revision.rev,
1745
self._max_chars, revision.tags, indent))
1631
self._max_chars, revision.tags, indent))
1746
1632
self.to_file.write('\n')
1748
1634
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1749
1635
"""Format log info into one string. Truncate tail of string
1751
:param revno: revision number or None.
1752
Revision numbers counts from 1.
1753
:param rev: revision object
1754
:param max_chars: maximum length of resulting string
1755
:param tags: list of tags or None
1756
:param prefix: string to prefix each line
1757
:return: formatted truncated string
1636
:param revno: revision number or None.
1637
Revision numbers counts from 1.
1638
:param rev: revision object
1639
:param max_chars: maximum length of resulting string
1640
:param tags: list of tags or None
1641
:param prefix: string to prefix each line
1642
:return: formatted truncated string
1761
1646
# show revno only when is not None
1762
1647
out.append("%s:" % revno)
1763
if max_chars is not None:
1764
out.append(self.truncate(
1765
self.short_author(rev), (max_chars + 3) // 4))
1767
out.append(self.short_author(rev))
1648
out.append(self.truncate(self.short_author(rev), 20))
1768
1649
out.append(self.date_string(rev))
1769
1650
if len(rev.parent_ids) > 1:
1770
1651
out.append('[merge]')
1772
tag_str = '{%s}' % (', '.join(sorted(tags)))
1653
tag_str = '{%s}' % (', '.join(tags))
1773
1654
out.append(tag_str)
1774
1655
out.append(rev.get_summary())
1775
1656
return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1862
1739
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1863
1740
except KeyError:
1864
raise errors.BzrCommandError(
1865
gettext("unknown log formatter: %r") % name)
1868
def author_list_all(rev):
1869
return rev.get_apparent_authors()[:]
1872
def author_list_first(rev):
1873
lst = rev.get_apparent_authors()
1880
def author_list_committer(rev):
1881
return [rev.committer]
1884
author_list_registry = registry.Registry()
1886
author_list_registry.register('all', author_list_all,
1889
author_list_registry.register('first', author_list_first,
1892
author_list_registry.register('committer', author_list_committer,
1741
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1744
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1745
# deprecated; for compatibility
1746
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1747
lf.show(revno, rev, delta)
1896
1750
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1913
1767
# This is the first index which is different between
1915
1769
base_idx = None
1916
for i in range(max(len(new_rh), len(old_rh))):
1770
for i in xrange(max(len(new_rh),
1917
1772
if (len(new_rh) <= i
1918
1773
or len(old_rh) <= i
1919
or new_rh[i] != old_rh[i]):
1774
or new_rh[i] != old_rh[i]):
1923
1778
if base_idx is None:
1924
1779
to_file.write('Nothing seems to have changed\n')
1926
# TODO: It might be nice to do something like show_log
1927
# and show the merged entries. But since this is the
1928
# removed revisions, it shouldn't be as important
1781
## TODO: It might be nice to do something like show_log
1782
## and show the merged entries. But since this is the
1783
## removed revisions, it shouldn't be as important
1929
1784
if base_idx < len(old_rh):
1930
to_file.write('*' * 60)
1785
to_file.write('*'*60)
1931
1786
to_file.write('\nRemoved Revisions:\n')
1932
1787
for i in range(base_idx, len(old_rh)):
1933
1788
rev = branch.repository.get_revision(old_rh[i])
1934
lr = LogRevision(rev, i + 1, 0, None)
1789
lr = LogRevision(rev, i+1, 0, None)
1935
1790
lf.log_revision(lr)
1936
to_file.write('*' * 60)
1791
to_file.write('*'*60)
1937
1792
to_file.write('\n\n')
1938
1793
if base_idx < len(new_rh):
1939
1794
to_file.write('Added Revisions:\n')
1940
1795
show_log(branch,
1943
1799
direction='forward',
1944
start_revision=base_idx + 1,
1800
start_revision=base_idx+1,
1945
1801
end_revision=len(new_rh),
2051
1907
:param file_list: the list of paths given on the command line;
2052
1908
the first of these can be a branch location or a file path,
2053
1909
the remainder must be file paths
2054
:param exit_stack: When the branch returned is read locked,
2055
an unlock call will be queued to the exit stack.
2056
1910
:return: (branch, info_list, start_rev_info, end_rev_info) where
2057
1911
info_list is a list of (relative_path, file_id, kind) tuples where
2058
1912
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2059
1913
branch will be read-locked.
2061
from breezy.builtins import _get_revision_range
2062
tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2064
exit_stack.enter_context(b.lock_read())
1915
from builtins import _get_revision_range, safe_relpath_files
1916
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2065
1918
# XXX: It's damn messy converting a list of paths to relative paths when
2066
1919
# those paths might be deleted ones, they might be on a case-insensitive
2067
1920
# filesystem and/or they might be in silly locations (like another branch).