101
100
TODO: Perhaps some way to limit this to only particular revisions,
102
101
or to traverse a non-mainline set of revisions?
104
last_verifier = last_tree.get_file_verifier(last_path)
105
graph = repository.get_graph()
106
history = list(graph.iter_lefthand_ancestry(last_revision, []))
108
for revision_id in history:
109
this_tree = repository.revision_tree(revision_id)
110
this_intertree = InterTree.get(this_tree, last_tree)
111
this_path = this_intertree.find_source_path(last_path)
106
for revision_id in branch.revision_history():
107
this_inv = branch.repository.get_revision_inventory(revision_id)
108
if file_id in this_inv:
109
this_ie = this_inv[file_id]
110
this_path = this_inv.id2path(file_id)
112
this_ie = this_path = None
113
114
# now we know how it was last time, and how it is in this revision.
114
115
# are those two states effectively the same or not?
115
if this_path is not None and last_path is None:
116
yield revno, revision_id, "deleted " + this_path
117
this_verifier = this_tree.get_file_verifier(this_path)
118
elif this_path is None and last_path is not None:
119
yield revno, revision_id, "added " + last_path
117
if not this_ie and not last_ie:
118
# not present in either
120
elif this_ie and not last_ie:
121
yield revno, revision_id, "added " + this_path
122
elif not this_ie and last_ie:
124
yield revno, revision_id, "deleted " + last_path
120
125
elif this_path != last_path:
121
yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
122
this_verifier = this_tree.get_file_verifier(this_path)
124
this_verifier = this_tree.get_file_verifier(this_path)
125
if (this_verifier != last_verifier):
126
yield revno, revision_id, "modified " + this_path
126
yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
127
elif (this_ie.text_size != last_ie.text_size
128
or this_ie.text_sha1 != last_ie.text_sha1):
129
yield revno, revision_id, "modified " + this_path
128
last_verifier = this_verifier
129
132
last_path = this_path
130
last_tree = this_tree
131
if last_path is None:
136
def _enumerate_history(branch):
139
for rev_id in branch.revision_history():
140
rh.append((revno, rev_id))
136
145
def show_log(branch,
147
specific_fileid=None,
139
149
direction='reverse',
140
150
start_revision=None,
141
151
end_revision=None,
145
155
"""Write out human-readable log of commits to this branch.
147
157
This function is being retained for backwards compatibility but
161
174
:param end_revision: If not None, only show revisions <= end_revision
176
:param search: If not None, only show revisions with matching commit
163
179
:param limit: If set, shows only 'limit' revisions, all revisions are shown
166
182
:param show_diff: If True, output a diff after each revision.
168
:param match: Dictionary of search lists to use when matching revision
184
# Convert old-style parameters to new-style parameters
185
if specific_fileid is not None:
186
file_ids = [specific_fileid]
191
delta_type = 'partial'
174
195
delta_type = None
198
diff_type = 'partial'
180
if isinstance(start_revision, int):
182
start_revision = revisionspec.RevisionInfo(branch, start_revision)
183
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
184
raise errors.InvalidRevisionNumber(start_revision)
186
if isinstance(end_revision, int):
188
end_revision = revisionspec.RevisionInfo(branch, end_revision)
189
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
190
raise errors.InvalidRevisionNumber(end_revision)
192
if end_revision is not None and end_revision.revno == 0:
193
raise errors.InvalidRevisionNumber(end_revision.revno)
195
204
# Build the request and execute it
196
rqst = make_log_request_dict(
205
rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
198
206
start_revision=start_revision, end_revision=end_revision,
199
limit=limit, delta_type=delta_type, diff_type=diff_type)
207
limit=limit, message_search=search,
208
delta_type=delta_type, diff_type=diff_type)
200
209
Logger(branch, rqst).show(lf)
203
# Note: This needs to be kept in sync with the defaults in
212
# Note: This needs to be kept this in sync with the defaults in
204
213
# make_log_request_dict() below
205
214
_DEFAULT_REQUEST_PARAMS = {
206
215
'direction': 'reverse',
208
217
'generate_tags': True,
209
'exclude_common_ancestry': False,
210
218
'_match_using_deltas': True,
214
def make_log_request_dict(direction='reverse', specific_files=None,
215
start_revision=None, end_revision=None, limit=None,
216
message_search=None, levels=None, generate_tags=True,
218
diff_type=None, _match_using_deltas=True,
219
exclude_common_ancestry=False, match=None,
220
signature=False, omit_merges=False,
222
def make_log_request_dict(direction='reverse', specific_fileids=None,
223
start_revision=None, end_revision=None, limit=None,
224
message_search=None, levels=1, generate_tags=True, delta_type=None,
225
diff_type=None, _match_using_deltas=True):
222
226
"""Convenience function for making a logging request dictionary.
224
228
Using this function may make code slightly safer by ensuring
244
248
matching commit messages
246
250
:param levels: the number of levels of revisions to
247
generate; 1 for just the mainline; 0 for all levels, or None for
251
generate; 1 for just the mainline; 0 for all levels.
250
253
:param generate_tags: If True, include tags for matched revisions.
252
255
:param delta_type: Either 'full', 'partial' or None.
253
256
'full' means generate the complete delta - adds/deletes/modifies/etc;
254
'partial' means filter the delta using specific_files;
257
'partial' means filter the delta using specific_fileids;
255
258
None means do not generate any delta.
257
260
:param diff_type: Either 'full', 'partial' or None.
258
261
'full' means generate the complete diff - adds/deletes/modifies/etc;
259
'partial' means filter the diff using specific_files;
262
'partial' means filter the diff using specific_fileids;
260
263
None means do not generate any diff.
262
265
:param _match_using_deltas: a private parameter controlling the
263
algorithm used for matching specific_files. This parameter
264
may be removed in the future so breezy client code should NOT
266
algorithm used for matching specific_fileids. This parameter
267
may be removed in the future so bzrlib client code should NOT
267
:param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
268
range operator or as a graph difference.
270
:param signature: show digital signature information
272
:param match: Dictionary of list of search strings to use when filtering
273
revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
274
the empty string to match any of the preceding properties.
276
:param omit_merges: If True, commits with more than one parent are
280
# Take care of old style message_search parameter
283
if 'message' in match:
284
match['message'].append(message_search)
286
match['message'] = [message_search]
288
match = {'message': [message_search]}
290
271
'direction': direction,
291
'specific_files': specific_files,
272
'specific_fileids': specific_fileids,
292
273
'start_revision': start_revision,
293
274
'end_revision': end_revision,
276
'message_search': message_search,
295
277
'levels': levels,
296
278
'generate_tags': generate_tags,
297
279
'delta_type': delta_type,
298
280
'diff_type': diff_type,
299
'exclude_common_ancestry': exclude_common_ancestry,
300
'signature': signature,
302
'omit_merges': omit_merges,
303
281
# Add 'private' attributes for features that may be deprecated
304
282
'_match_using_deltas': _match_using_deltas,
381
341
# Tweak the LogRequest based on what the LogFormatter can handle.
382
342
# (There's no point generating stuff if the formatter can't display it.)
384
if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
385
# user didn't specify levels, use whatever the LF can handle:
386
rqst['levels'] = lf.get_levels()
344
rqst['levels'] = lf.get_levels()
388
345
if not getattr(lf, 'supports_tags', False):
389
346
rqst['generate_tags'] = False
390
347
if not getattr(lf, 'supports_delta', False):
391
348
rqst['delta_type'] = None
392
349
if not getattr(lf, 'supports_diff', False):
393
350
rqst['diff_type'] = None
394
if not getattr(lf, 'supports_signatures', False):
395
rqst['signature'] = False
397
352
# Find and print the interesting revisions
398
353
generator = self._generator_factory(self.branch, rqst)
400
for lr in generator.iter_log_revisions():
402
except errors.GhostRevisionUnusableHere:
403
raise errors.CommandError(
404
gettext('Further revision history missing.'))
354
for lr in generator.iter_log_revisions():
407
358
def _generator_factory(self, branch, rqst):
408
359
"""Make the LogGenerator object to use.
410
361
Subclasses may wish to override this.
412
return _DefaultLogGenerator(branch, **rqst)
415
def _log_revision_iterator_using_per_file_graph(
416
branch, delta_type, match, levels, path, start_rev_id, end_rev_id,
417
direction, exclude_common_ancestry):
418
# Get the base revisions, filtering by the revision range.
419
# Note that we always generate the merge revisions because
420
# filter_revisions_touching_path() requires them ...
421
view_revisions = _calc_view_revisions(
422
branch, start_rev_id, end_rev_id,
423
direction, generate_merge_revisions=True,
424
exclude_common_ancestry=exclude_common_ancestry)
425
if not isinstance(view_revisions, list):
426
view_revisions = list(view_revisions)
427
view_revisions = _filter_revisions_touching_path(
428
branch, path, view_revisions,
429
include_merges=levels != 1)
430
return make_log_rev_iterator(
431
branch, view_revisions, delta_type, match)
434
def _log_revision_iterator_using_delta_matching(
435
branch, delta_type, match, levels, specific_files, start_rev_id, end_rev_id,
436
direction, exclude_common_ancestry, limit):
437
# Get the base revisions, filtering by the revision range
438
generate_merge_revisions = levels != 1
439
delayed_graph_generation = not specific_files and (
440
limit or start_rev_id or end_rev_id)
441
view_revisions = _calc_view_revisions(
442
branch, start_rev_id, end_rev_id,
444
generate_merge_revisions=generate_merge_revisions,
445
delayed_graph_generation=delayed_graph_generation,
446
exclude_common_ancestry=exclude_common_ancestry)
448
# Apply the other filters
449
return make_log_rev_iterator(branch, view_revisions,
451
files=specific_files,
455
def _format_diff(branch, rev, diff_type, files=None):
458
:param branch: Branch object
459
:param rev: Revision object
460
:param diff_type: Type of diff to generate
461
:param files: List of files to generate diff for (or None for all)
463
repo = branch.repository
464
if len(rev.parent_ids) == 0:
465
ancestor_id = _mod_revision.NULL_REVISION
467
ancestor_id = rev.parent_ids[0]
468
tree_1 = repo.revision_tree(ancestor_id)
469
tree_2 = repo.revision_tree(rev.revision_id)
470
if diff_type == 'partial' and files is not None:
471
specific_files = files
473
specific_files = None
475
path_encoding = get_diff_header_encoding()
476
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
477
new_label='', path_encoding=path_encoding)
363
return _DefaultLogGenerator(branch, rqst)
481
366
class _StartNotLinearAncestor(Exception):
517
384
:return: An iterator yielding LogRevision objects.
520
388
revision_iterator = self._create_log_revision_iterator()
521
389
for revs in revision_iterator:
522
390
for (rev_id, revno, merge_depth), rev, delta in revs:
523
391
# 0 levels means show everything; merge_depth counts from 0
524
if (self.levels != 0 and merge_depth is not None and
525
merge_depth >= self.levels):
527
if self.omit_merges and len(rev.parent_ids) > 1:
530
raise errors.GhostRevisionUnusableHere(rev_id)
531
if self.diff_type is None:
535
self.branch, rev, self.diff_type,
537
if self.show_signature:
538
signature = format_signature_validity(rev_id, self.branch)
542
rev, revno, merge_depth, delta,
543
self.rev_tag_dict.get(rev_id), diff, signature)
392
levels = rqst.get('levels')
393
if levels != 0 and merge_depth >= levels:
395
diff = self._format_diff(rev, rev_id)
396
yield LogRevision(rev, revno, merge_depth, delta,
397
self.rev_tag_dict.get(rev_id), diff)
398
limit = rqst.get('limit')
546
if log_count >= self.limit:
401
if log_count >= limit:
404
def _format_diff(self, rev, rev_id):
405
diff_type = self.rqst.get('diff_type')
406
if diff_type is None:
408
repo = self.branch.repository
409
if len(rev.parent_ids) == 0:
410
ancestor_id = _mod_revision.NULL_REVISION
412
ancestor_id = rev.parent_ids[0]
413
tree_1 = repo.revision_tree(ancestor_id)
414
tree_2 = repo.revision_tree(rev_id)
415
file_ids = self.rqst.get('specific_fileids')
416
if diff_type == 'partial' and file_ids is not None:
417
specific_files = [tree_2.id2path(id) for id in file_ids]
419
specific_files = None
421
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
549
425
def _create_log_revision_iterator(self):
550
426
"""Create a revision iterator for log.
552
428
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
555
start_rev_id, end_rev_id = _get_revision_limits(
556
self.branch, self.start_revision, self.end_revision)
557
if self._match_using_deltas:
558
return _log_revision_iterator_using_delta_matching(
560
delta_type=self.delta_type,
563
specific_files=self.specific_files,
564
start_rev_id=start_rev_id, end_rev_id=end_rev_id,
565
direction=self.direction,
566
exclude_common_ancestry=self.exclude_common_ancestry,
431
self.start_rev_id, self.end_rev_id = _get_revision_limits(
432
self.branch, self.rqst.get('start_revision'),
433
self.rqst.get('end_revision'))
434
if self.rqst.get('_match_using_deltas'):
435
return self._log_revision_iterator_using_delta_matching()
569
437
# We're using the per-file-graph algorithm. This scales really
570
438
# well but only makes sense if there is a single file and it's
571
439
# not a directory
572
file_count = len(self.specific_files)
440
file_count = len(self.rqst.get('specific_fileids'))
573
441
if file_count != 1:
574
raise errors.BzrError(
575
"illegal LogRequest: must match-using-deltas "
442
raise BzrError("illegal LogRequest: must match-using-deltas "
576
443
"when logging %d files" % file_count)
577
return _log_revision_iterator_using_per_file_graph(
579
delta_type=self.delta_type,
582
path=self.specific_files[0],
583
start_rev_id=start_rev_id, end_rev_id=end_rev_id,
584
direction=self.direction,
585
exclude_common_ancestry=self.exclude_common_ancestry
444
return self._log_revision_iterator_using_per_file_graph()
446
def _log_revision_iterator_using_delta_matching(self):
447
# Get the base revisions, filtering by the revision range
449
generate_merge_revisions = rqst.get('levels') != 1
450
delayed_graph_generation = not rqst.get('specific_fileids') and (
451
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
452
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
453
self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
454
delayed_graph_generation=delayed_graph_generation)
456
# Apply the other filters
457
return make_log_rev_iterator(self.branch, view_revisions,
458
rqst.get('delta_type'), rqst.get('message_search'),
459
file_ids=rqst.get('specific_fileids'),
460
direction=rqst.get('direction'))
462
def _log_revision_iterator_using_per_file_graph(self):
463
# Get the base revisions, filtering by the revision range.
464
# Note that we always generate the merge revisions because
465
# filter_revisions_touching_file_id() requires them ...
467
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
468
self.end_rev_id, rqst.get('direction'), True)
469
if not isinstance(view_revisions, list):
470
view_revisions = list(view_revisions)
471
view_revisions = _filter_revisions_touching_file_id(self.branch,
472
rqst.get('specific_fileids')[0], view_revisions,
473
include_merges=rqst.get('levels') != 1)
474
return make_log_rev_iterator(self.branch, view_revisions,
475
rqst.get('delta_type'), rqst.get('message_search'))
589
478
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
590
generate_merge_revisions,
591
delayed_graph_generation=False,
592
exclude_common_ancestry=False,
479
generate_merge_revisions, delayed_graph_generation=False):
594
480
"""Calculate the revisions to view.
596
482
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
597
483
a list of the same tuples.
599
if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
raise errors.CommandError(gettext(
601
'--exclude-common-ancestry requires two different revisions'))
602
if direction not in ('reverse', 'forward'):
603
raise ValueError(gettext('invalid direction %r') % direction)
604
br_rev_id = branch.last_revision()
605
if br_rev_id == _mod_revision.NULL_REVISION:
485
br_revno, br_rev_id = branch.last_revision_info()
608
if (end_rev_id and start_rev_id == end_rev_id
609
and (not generate_merge_revisions
610
or not _has_merges(branch, end_rev_id))):
611
# If a single revision is requested, check we can handle it
612
return _generate_one_revision(branch, end_rev_id, br_rev_id,
489
# If a single revision is requested, check we can handle it
490
generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
491
(not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
492
if generate_single_revision:
493
return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
495
# If we only want to see linear revisions, we can iterate ...
614
496
if not generate_merge_revisions:
616
# If we only want to see linear revisions, we can iterate ...
617
iter_revs = _linear_view_revisions(
618
branch, start_rev_id, end_rev_id,
619
exclude_common_ancestry=exclude_common_ancestry)
620
# If a start limit was given and it's not obviously an
621
# ancestor of the end limit, check it before outputting anything
622
if (direction == 'forward'
623
or (start_rev_id and not _is_obvious_ancestor(
624
branch, start_rev_id, end_rev_id))):
625
iter_revs = list(iter_revs)
626
if direction == 'forward':
627
iter_revs = reversed(iter_revs)
629
except _StartNotLinearAncestor:
630
# Switch to the slower implementation that may be able to find a
631
# non-obvious ancestor out of the left-hand history.
633
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
634
direction, delayed_graph_generation,
635
exclude_common_ancestry)
636
if direction == 'forward':
637
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
497
return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
500
return _generate_all_revisions(branch, start_rev_id, end_rev_id,
501
direction, delayed_graph_generation)
641
504
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
644
507
return [(br_rev_id, br_revno, 0)]
646
revno_str = _compute_revno_str(branch, rev_id)
509
revno = branch.revision_id_to_dotted_revno(rev_id)
510
revno_str = '.'.join(str(n) for n in revno)
647
511
return [(rev_id, revno_str, 0)]
514
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
515
result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
516
# If a start limit was given and it's not obviously an
517
# ancestor of the end limit, check it before outputting anything
518
if direction == 'forward' or (start_rev_id
519
and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
521
result = list(result)
522
except _StartNotLinearAncestor:
523
raise errors.BzrCommandError('Start revision not found in'
524
' left-hand history of end revision.')
525
if direction == 'forward':
526
result = reversed(result)
650
530
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
651
delayed_graph_generation,
652
exclude_common_ancestry=False):
531
delayed_graph_generation):
653
532
# On large trees, generating the merge graph can take 30-60 seconds
654
533
# so we delay doing it until a merge is detected, incrementally
655
534
# returning initial (non-merge) revisions while we can.
657
# The above is only true for old formats (<= 0.92), for newer formats, a
658
# couple of seconds only should be needed to load the whole graph and the
659
# other graph operations needed are even faster than that -- vila 100201
660
535
initial_revisions = []
661
536
if delayed_graph_generation:
663
for rev_id, revno, depth in _linear_view_revisions(
664
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
538
for rev_id, revno, depth in \
539
_linear_view_revisions(branch, start_rev_id, end_rev_id):
665
540
if _has_merges(branch, rev_id):
666
# The end_rev_id can be nested down somewhere. We need an
667
# explicit ancestry check. There is an ambiguity here as we
668
# may not raise _StartNotLinearAncestor for a revision that
669
# is an ancestor but not a *linear* one. But since we have
670
# loaded the graph to do the check (or calculate a dotted
671
# revno), we may as well accept to show the log... We need
672
# the check only if start_rev_id is not None as all
673
# revisions have _mod_revision.NULL_REVISION as an ancestor
675
graph = branch.repository.get_graph()
676
if (start_rev_id is not None
677
and not graph.is_ancestor(start_rev_id, end_rev_id)):
678
raise _StartNotLinearAncestor()
679
# Since we collected the revisions so far, we need to
681
541
end_rev_id = rev_id
684
544
initial_revisions.append((rev_id, revno, depth))
686
546
# No merged revisions found
687
return initial_revisions
547
if direction == 'reverse':
548
return initial_revisions
549
elif direction == 'forward':
550
return reversed(initial_revisions)
552
raise ValueError('invalid direction %r' % direction)
688
553
except _StartNotLinearAncestor:
689
554
# A merge was never detected so the lower revision limit can't
690
555
# be nested down somewhere
691
raise errors.CommandError(gettext('Start revision not found in'
692
' history of end revision.'))
694
# We exit the loop above because we encounter a revision with merges, from
695
# this revision, we need to switch to _graph_view_revisions.
556
raise errors.BzrCommandError('Start revision not found in'
557
' history of end revision.')
697
559
# A log including nested merges is required. If the direction is reverse,
698
560
# we rebase the initial merge depths so that the development line is
699
561
# shown naturally, i.e. just like it is for linear logging. We can easily
700
562
# make forward the exact opposite display, but showing the merge revisions
701
563
# indented at the end seems slightly nicer in that case.
702
view_revisions = itertools.chain(iter(initial_revisions),
703
_graph_view_revisions(branch, start_rev_id, end_rev_id,
704
rebase_initial_depths=(
705
direction == 'reverse'),
706
exclude_common_ancestry=exclude_common_ancestry))
707
return view_revisions
564
view_revisions = chain(iter(initial_revisions),
565
_graph_view_revisions(branch, start_rev_id, end_rev_id,
566
rebase_initial_depths=direction == 'reverse'))
567
if direction == 'reverse':
568
return view_revisions
569
elif direction == 'forward':
570
# Forward means oldest first, adjusting for depth.
571
view_revisions = reverse_by_depth(list(view_revisions))
572
return _rebase_merge_depth(view_revisions)
574
raise ValueError('invalid direction %r' % direction)
710
577
def _has_merges(branch, rev_id):
713
580
return len(parents) > 1
716
def _compute_revno_str(branch, rev_id):
717
"""Compute the revno string from a rev_id.
719
:return: The revno string, or None if the revision is not in the supplied
723
revno = branch.revision_id_to_dotted_revno(rev_id)
724
except errors.NoSuchRevision:
725
# The revision must be outside of this branch
728
return '.'.join(str(n) for n in revno)
731
583
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
732
584
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
733
585
if start_rev_id and end_rev_id:
735
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
736
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
737
except errors.NoSuchRevision:
738
# one or both is not in the branch; not obvious
586
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
587
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
740
588
if len(start_dotted) == 1 and len(end_dotted) == 1:
741
589
# both on mainline
742
590
return start_dotted[0] <= end_dotted[0]
743
591
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
744
start_dotted[0:1] == end_dotted[0:1]):
592
start_dotted[0:1] == end_dotted[0:1]):
745
593
# both on same development line
746
594
return start_dotted[2] <= end_dotted[2]
750
# if either start or end is not specified then we use either the first or
751
# the last revision and *they* are obvious ancestors.
755
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
756
exclude_common_ancestry=False):
601
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
757
602
"""Calculate a sequence of revisions to view, newest to oldest.
759
604
:param start_rev_id: the lower revision-id
760
605
:param end_rev_id: the upper revision-id
761
:param exclude_common_ancestry: Whether the start_rev_id should be part of
762
the iterated revisions.
763
606
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
764
dotted_revno will be None for ghosts
765
607
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
766
is not found walking the left-hand history
608
is not found walking the left-hand history
610
br_revno, br_rev_id = branch.last_revision_info()
768
611
repo = branch.repository
769
graph = repo.get_graph()
770
612
if start_rev_id is None and end_rev_id is None:
771
if branch._format.stores_revno() or \
772
config.GlobalStack().get('calculate_revnos'):
774
br_revno, br_rev_id = branch.last_revision_info()
775
except errors.GhostRevisionsHaveNoRevno:
776
br_rev_id = branch.last_revision()
781
br_rev_id = branch.last_revision()
784
graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
(_mod_revision.NULL_REVISION,))
788
revision_id = next(graph_iter)
789
except errors.RevisionNotPresent as e:
791
yield e.revision_id, None, None
793
except StopIteration:
796
yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
797
if cur_revno is not None:
614
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
615
yield revision_id, str(cur_revno), 0
800
br_rev_id = branch.last_revision()
801
618
if end_rev_id is None:
802
619
end_rev_id = br_rev_id
803
620
found_start = start_rev_id is None
804
graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
805
(_mod_revision.NULL_REVISION,))
808
revision_id = next(graph_iter)
809
except StopIteration:
811
except errors.RevisionNotPresent as e:
813
yield e.revision_id, None, None
621
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
622
revno = branch.revision_id_to_dotted_revno(revision_id)
623
revno_str = '.'.join(str(n) for n in revno)
624
if not found_start and revision_id == start_rev_id:
625
yield revision_id, revno_str, 0
816
revno_str = _compute_revno_str(branch, revision_id)
817
if not found_start and revision_id == start_rev_id:
818
if not exclude_common_ancestry:
819
yield revision_id, revno_str, 0
823
yield revision_id, revno_str, 0
825
raise _StartNotLinearAncestor()
629
yield revision_id, revno_str, 0
632
raise _StartNotLinearAncestor()
828
635
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
829
rebase_initial_depths=True,
830
exclude_common_ancestry=False):
636
rebase_initial_depths=True):
831
637
"""Calculate revisions to view including merges, newest to oldest.
833
639
:param branch: the branch
860
662
depth_adjustment = merge_depth
861
663
if depth_adjustment:
862
664
if merge_depth < depth_adjustment:
863
# From now on we reduce the depth adjustement, this can be
864
# surprising for users. The alternative requires two passes
865
# which breaks the fast display of the first revision
867
665
depth_adjustment = merge_depth
868
666
merge_depth -= depth_adjustment
869
667
yield rev_id, '.'.join(map(str, revno)), merge_depth
670
def calculate_view_revisions(branch, start_revision, end_revision, direction,
671
specific_fileid, generate_merge_revisions):
672
"""Calculate the revisions to view.
674
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
675
a list of the same tuples.
677
# This method is no longer called by the main code path.
678
# It is retained for API compatibility and may be deprecated
680
start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
682
view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
683
direction, generate_merge_revisions or specific_fileid))
685
view_revisions = _filter_revisions_touching_file_id(branch,
686
specific_fileid, view_revisions,
687
include_merges=generate_merge_revisions)
688
return _rebase_merge_depth(view_revisions)
872
691
def _rebase_merge_depth(view_revisions):
873
692
"""Adjust depths upwards so the top level is 0."""
874
693
# If either the first or last revision have a merge_depth of 0, we're done
875
694
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
876
min_depth = min([d for r, n, d in view_revisions])
695
min_depth = min([d for r,n,d in view_revisions])
877
696
if min_depth != 0:
878
view_revisions = [(r, n, d - min_depth)
879
for r, n, d in view_revisions]
697
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
880
698
return view_revisions
883
701
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
884
files=None, direction='reverse'):
702
file_ids=None, direction='reverse'):
885
703
"""Create a revision iterator for log.
887
705
:param branch: The branch being logged.
911
729
# It would be nicer if log adapters were first class objects
912
730
# with custom parameters. This will do for now. IGC 20090127
913
731
if adapter == _make_delta_filter:
914
log_rev_iterator = adapter(
915
branch, generate_delta, search, log_rev_iterator, files,
732
log_rev_iterator = adapter(branch, generate_delta,
733
search, log_rev_iterator, file_ids, direction)
918
log_rev_iterator = adapter(
919
branch, generate_delta, search, log_rev_iterator)
735
log_rev_iterator = adapter(branch, generate_delta,
736
search, log_rev_iterator)
920
737
return log_rev_iterator
923
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
740
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
924
741
"""Create a filtered iterator of log_rev_iterator matching on a regex.
926
743
:param branch: The branch being logged.
927
744
:param generate_delta: Whether to generate a delta for each revision.
928
:param match: A dictionary with properties as keys and lists of strings
929
as values. To match, a revision may match any of the supplied strings
930
within a single property but must match at least one string for each
745
:param search: A user text search string.
932
746
:param log_rev_iterator: An input iterator containing all revisions that
933
747
could be displayed, in lists.
934
748
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
938
752
return log_rev_iterator
939
# Use lazy_compile so mapping to InvalidPattern error occurs.
940
searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
941
for k, v in match.items()]
942
return _filter_re(searchRE, log_rev_iterator)
945
def _filter_re(searchRE, log_rev_iterator):
753
searchRE = re_compile_checked(search, re.IGNORECASE,
754
'log message filter')
755
return _filter_message_re(searchRE, log_rev_iterator)
758
def _filter_message_re(searchRE, log_rev_iterator):
946
759
for revs in log_rev_iterator:
947
new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
952
def _match_filter(searchRE, rev):
954
'message': (rev.message,),
955
'committer': (rev.committer,),
956
'author': (rev.get_apparent_authors()),
957
'bugs': list(rev.iter_bugs())
959
strings[''] = [item for inner_list in strings.values()
960
for item in inner_list]
961
for k, v in searchRE:
962
if k in strings and not _match_any_filter(strings[k], v):
967
def _match_any_filter(strings, res):
968
return any(r.search(s) for r in res for s in strings)
761
for (rev_id, revno, merge_depth), rev, delta in revs:
762
if searchRE.search(rev.message):
763
new_revs.append(((rev_id, revno, merge_depth), rev, delta))
971
767
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
files=None, direction='reverse'):
768
fileids=None, direction='reverse'):
973
769
"""Add revision deltas to a log iterator if needed.
975
771
:param branch: The branch being logged.
978
774
:param search: A user text search string.
979
775
:param log_rev_iterator: An input iterator containing all revisions that
980
776
could be displayed, in lists.
981
:param files: If non empty, only revisions matching one or more of
982
the files are to be kept.
777
:param fileids: If non empty, only revisions matching one or more of
778
the file-ids are to be kept.
983
779
:param direction: the direction in which view_revisions is sorted
984
780
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
987
if not generate_delta and not files:
783
if not generate_delta and not fileids:
988
784
return log_rev_iterator
989
785
return _generate_deltas(branch.repository, log_rev_iterator,
990
generate_delta, files, direction)
993
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
786
generate_delta, fileids, direction)
789
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
995
791
"""Create deltas for each batch of revisions in log_rev_iterator.
997
793
If we're only generating deltas for the sake of filtering against
998
files, we stop generating deltas once all files reach the
794
file-ids, we stop generating deltas once all file-ids reach the
999
795
appropriate life-cycle point. If we're receiving data newest to
1000
796
oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1002
check_files = files is not None and len(files) > 0
1004
file_set = set(files)
798
check_fileids = fileids is not None and len(fileids) > 0
800
fileid_set = set(fileids)
1005
801
if direction == 'reverse':
1008
804
stop_on = 'remove'
1011
807
for revs in log_rev_iterator:
1012
# If we were matching against files and we've run out,
808
# If we were matching against fileids and we've run out,
1013
809
# there's nothing left to do
1014
if check_files and not file_set:
810
if check_fileids and not fileid_set:
1016
812
revisions = [rev[1] for rev in revs]
1018
if delta_type == 'full' and not check_files:
1019
deltas = repository.get_revision_deltas(revisions)
1020
for rev, delta in zip(revs, deltas):
814
if delta_type == 'full' and not check_fileids:
815
deltas = repository.get_deltas_for_revisions(revisions)
816
for rev, delta in izip(revs, deltas):
1021
817
new_revs.append((rev[0], rev[1], delta))
1023
deltas = repository.get_revision_deltas(
1024
revisions, specific_files=file_set)
1025
for rev, delta in zip(revs, deltas):
819
deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
820
for rev, delta in izip(revs, deltas):
1027
822
if delta is None or not delta.has_changed():
1030
_update_files(delta, file_set, stop_on)
825
_update_fileids(delta, fileid_set, stop_on)
1031
826
if delta_type is None:
1033
828
elif delta_type == 'full':
1123
903
def _get_revision_limits(branch, start_revision, end_revision):
1124
904
"""Get and check revision limits.
1126
:param branch: The branch containing the revisions.
1128
:param start_revision: The first revision to be logged, as a RevisionInfo.
1130
:param end_revision: The last revision to be logged, as a RevisionInfo
906
:param branch: The branch containing the revisions.
908
:param start_revision: The first revision to be logged.
909
For backwards compatibility this may be a mainline integer revno,
910
but for merge revision support a RevisionInfo is expected.
912
:param end_revision: The last revision to be logged.
913
For backwards compatibility this may be a mainline integer revno,
914
but for merge revision support a RevisionInfo is expected.
1132
916
:return: (start_rev_id, end_rev_id) tuple.
918
branch_revno, branch_rev_id = branch.last_revision_info()
1134
919
start_rev_id = None
1136
if start_revision is not None:
1137
if not isinstance(start_revision, revisionspec.RevisionInfo):
1138
raise TypeError(start_revision)
1139
start_rev_id = start_revision.rev_id
1140
start_revno = start_revision.revno
1141
if start_revno is None:
920
if start_revision is None:
923
if isinstance(start_revision, revisionspec.RevisionInfo):
924
start_rev_id = start_revision.rev_id
925
start_revno = start_revision.revno or 1
927
branch.check_real_revno(start_revision)
928
start_revno = start_revision
929
start_rev_id = branch.get_rev_id(start_revno)
1144
931
end_rev_id = None
1146
if end_revision is not None:
1147
if not isinstance(end_revision, revisionspec.RevisionInfo):
1148
raise TypeError(start_revision)
1149
end_rev_id = end_revision.rev_id
1150
end_revno = end_revision.revno
932
if end_revision is None:
933
end_revno = branch_revno
935
if isinstance(end_revision, revisionspec.RevisionInfo):
936
end_rev_id = end_revision.rev_id
937
end_revno = end_revision.revno or branch_revno
939
branch.check_real_revno(end_revision)
940
end_revno = end_revision
941
end_rev_id = branch.get_rev_id(end_revno)
1152
if branch.last_revision() != _mod_revision.NULL_REVISION:
943
if branch_revno != 0:
1153
944
if (start_rev_id == _mod_revision.NULL_REVISION
1154
or end_rev_id == _mod_revision.NULL_REVISION):
1155
raise errors.CommandError(
1156
gettext('Logging revision 0 is invalid.'))
1157
if end_revno is not None and start_revno > end_revno:
1158
raise errors.CommandError(
1159
gettext("Start revision must be older than the end revision."))
945
or end_rev_id == _mod_revision.NULL_REVISION):
946
raise errors.BzrCommandError('Logging revision 0 is invalid.')
947
if start_revno > end_revno:
948
raise errors.BzrCommandError("Start revision must be older than "
1160
950
return (start_rev_id, end_rev_id)
1243
1032
return mainline_revs, rev_nos, start_rev_id, end_rev_id
1246
def _filter_revisions_touching_path(branch, path, view_revisions,
1247
include_merges=True):
1248
r"""Return the list of revision ids which touch a given path.
1035
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1036
"""Filter view_revisions based on revision ranges.
1038
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1039
tuples to be filtered.
1041
:param start_rev_id: If not NONE specifies the first revision to be logged.
1042
If NONE then all revisions up to the end_rev_id are logged.
1044
:param end_rev_id: If not NONE specifies the last revision to be logged.
1045
If NONE then all revisions up to the end of the log are logged.
1047
:return: The filtered view_revisions.
1049
# This method is no longer called by the main code path.
1050
# It may be removed soon. IGC 20090127
1051
if start_rev_id or end_rev_id:
1052
revision_ids = [r for r, n, d in view_revisions]
1054
start_index = revision_ids.index(start_rev_id)
1057
if start_rev_id == end_rev_id:
1058
end_index = start_index
1061
end_index = revision_ids.index(end_rev_id)
1063
end_index = len(view_revisions) - 1
1064
# To include the revisions merged into the last revision,
1065
# extend end_rev_id down to, but not including, the next rev
1066
# with the same or lesser merge_depth
1067
end_merge_depth = view_revisions[end_index][2]
1069
for index in xrange(end_index+1, len(view_revisions)+1):
1070
if view_revisions[index][2] <= end_merge_depth:
1071
end_index = index - 1
1074
# if the search falls off the end then log to the end as well
1075
end_index = len(view_revisions) - 1
1076
view_revisions = view_revisions[start_index:end_index+1]
1077
return view_revisions
1080
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1081
include_merges=True):
1082
r"""Return the list of revision ids which touch a given file id.
1250
1084
The function filters view_revisions and returns a subset.
1251
This includes the revisions which directly change the path,
1085
This includes the revisions which directly change the file id,
1252
1086
and the revisions which merge these changes. So if the
1253
1087
revision graph is::
1162
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1163
include_merges=True):
1164
"""Produce an iterator of revisions to show
1165
:return: an iterator of (revision_id, revno, merge_depth)
1166
(if there is no revno for a revision, None is supplied)
1168
# This method is no longer called by the main code path.
1169
# It is retained for API compatibility and may be deprecated
1170
# soon. IGC 20090127
1171
if not include_merges:
1172
revision_ids = mainline_revs[1:]
1173
if direction == 'reverse':
1174
revision_ids.reverse()
1175
for revision_id in revision_ids:
1176
yield revision_id, str(rev_nos[revision_id]), 0
1178
graph = branch.repository.get_graph()
1179
# This asks for all mainline revisions, which means we only have to spider
1180
# sideways, rather than depth history. That said, its still size-of-history
1181
# and should be addressed.
1182
# mainline_revisions always includes an extra revision at the beginning, so
1184
parent_map = dict(((key, value) for key, value in
1185
graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1186
# filter out ghosts; merge_sort errors on ghosts.
1187
rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1188
merge_sorted_revisions = tsort.merge_sort(
1192
generate_revno=True)
1194
if direction == 'forward':
1195
# forward means oldest first.
1196
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1197
elif direction != 'reverse':
1198
raise ValueError('invalid direction %r' % direction)
1200
for (sequence, rev_id, merge_depth, revno, end_of_merge
1201
) in merge_sorted_revisions:
1202
yield rev_id, '.'.join(map(str, revno)), merge_depth
1332
1205
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1333
1206
"""Reverse revisions by depth.
1335
1208
Revisions with a different depth are sorted as a group with the previous
1336
revision of that depth. There may be no topological justification for this
1209
revision of that depth. There may be no topological justification for this,
1337
1210
but it looks much nicer.
1339
1212
# Add a fake revision at start so that we can always attach sub revisions
1549
def show_properties(self, revision, indent):
1550
"""Displays the custom properties returned by each registered handler.
1552
If a registered handler raises an error it is propagated.
1554
for line in self.custom_properties(revision):
1555
self.to_file.write("%s%s\n" % (indent, line))
1557
def custom_properties(self, revision):
1558
"""Format the custom properties returned by each registered handler.
1560
If a registered handler raises an error it is propagated.
1562
:return: a list of formatted lines (excluding trailing newlines)
1564
lines = self._foreign_info_properties(revision)
1565
for key, handler in properties_handler_registry.iteritems():
1567
lines.extend(self._format_properties(handler(revision)))
1569
trace.log_exception_quietly()
1570
trace.print_exception(sys.exc_info(), self.to_file)
1573
def _foreign_info_properties(self, rev):
1370
def show_foreign_info(self, rev, indent):
1574
1371
"""Custom log displayer for foreign revision identifiers.
1576
1373
:param rev: Revision object.
1578
1375
# Revision comes directly from a foreign repository
1579
1376
if isinstance(rev, foreign.ForeignRevision):
1580
return self._format_properties(
1581
rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1377
self._write_properties(indent, rev.mapping.vcs.show_foreign_revid(
1583
1381
# Imported foreign revision revision ids always contain :
1584
if b":" not in rev.revision_id:
1382
if not ":" in rev.revision_id:
1587
1385
# Revision was once imported from a foreign repository
1589
1387
foreign_revid, mapping = \
1590
1388
foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1591
1389
except errors.InvalidRevisionId:
1594
return self._format_properties(
1392
self._write_properties(indent,
1595
1393
mapping.vcs.show_foreign_revid(foreign_revid))
1597
def _format_properties(self, properties):
1395
def show_properties(self, revision, indent):
1396
"""Displays the custom properties returned by each registered handler.
1398
If a registered handler raises an error it is propagated.
1400
for key, handler in properties_handler_registry.iteritems():
1401
self._write_properties(indent, handler(revision))
1403
def _write_properties(self, indent, properties):
1599
1404
for key, value in properties.items():
1600
lines.append(key + ': ' + value)
1405
self.to_file.write(indent + key + ': ' + value + '\n')
1603
1407
def show_diff(self, to_file, diff, indent):
1604
encoding = get_terminal_encoding()
1605
for l in diff.rstrip().split(b'\n'):
1606
to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
1609
# Separator between revisions in long format
1610
_LONG_SEP = '-' * 60
1408
for l in diff.rstrip().split('\n'):
1409
to_file.write(indent + '%s\n' % (l,))
1613
1412
class LongLogFormatter(LogFormatter):
1617
1416
supports_delta = True
1618
1417
supports_tags = True
1619
1418
supports_diff = True
1620
supports_signatures = True
1622
def __init__(self, *args, **kwargs):
1623
super(LongLogFormatter, self).__init__(*args, **kwargs)
1624
if self.show_timezone == 'original':
1625
self.date_string = self._date_string_original_timezone
1627
self.date_string = self._date_string_with_timezone
1629
def _date_string_with_timezone(self, rev):
1630
return format_date(rev.timestamp, rev.timezone or 0,
1633
def _date_string_original_timezone(self, rev):
1634
return format_date_with_offset_in_original_timezone(rev.timestamp,
1637
1420
def log_revision(self, revision):
1638
1421
"""Log a revision, either merged or not."""
1639
1422
indent = ' ' * revision.merge_depth
1423
to_file = self.to_file
1424
to_file.write(indent + '-' * 60 + '\n')
1641
1425
if revision.revno is not None:
1642
lines.append('revno: %s%s' % (revision.revno,
1643
self.merge_marker(revision)))
1426
to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
1427
self.merge_marker(revision)))
1644
1428
if revision.tags:
1645
lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
1646
if self.show_ids or revision.revno is None:
1647
lines.append('revision-id: %s' %
1648
(revision.rev.revision_id.decode('utf-8'),))
1429
to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1649
1430
if self.show_ids:
1431
to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1650
1433
for parent_id in revision.rev.parent_ids:
1651
lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1652
lines.extend(self.custom_properties(revision.rev))
1434
to_file.write(indent + 'parent: %s\n' % (parent_id,))
1435
self.show_foreign_info(revision.rev, indent)
1436
self.show_properties(revision.rev, indent)
1654
1438
committer = revision.rev.committer
1655
authors = self.authors(revision.rev, 'all')
1439
authors = revision.rev.get_apparent_authors()
1656
1440
if authors != [committer]:
1657
lines.append('author: %s' % (", ".join(authors),))
1658
lines.append('committer: %s' % (committer,))
1441
to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1442
to_file.write(indent + 'committer: %s\n' % (committer,))
1660
1444
branch_nick = revision.rev.properties.get('branch-nick', None)
1661
1445
if branch_nick is not None:
1662
lines.append('branch nick: %s' % (branch_nick,))
1664
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1666
if revision.signature is not None:
1667
lines.append('signature: ' + revision.signature)
1669
lines.append('message:')
1446
to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
1448
date_str = format_date(revision.rev.timestamp,
1449
revision.rev.timezone or 0,
1451
to_file.write(indent + 'timestamp: %s\n' % (date_str,))
1453
to_file.write(indent + 'message:\n')
1670
1454
if not revision.rev.message:
1671
lines.append(' (no message)')
1455
to_file.write(indent + ' (no message)\n')
1673
1457
message = revision.rev.message.rstrip('\r\n')
1674
1458
for l in message.split('\n'):
1675
lines.append(' %s' % (l,))
1677
# Dump the output, appending the delta and diff if requested
1678
to_file = self.to_file
1679
to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1459
to_file.write(indent + ' %s\n' % (l,))
1680
1460
if revision.delta is not None:
1681
# Use the standard status output to display changes
1682
from breezy.delta import report_delta
1683
report_delta(to_file, revision.delta, short_status=False,
1684
show_ids=self.show_ids, indent=indent)
1461
# We don't respect delta_format for compatibility
1462
revision.delta.show(to_file, self.show_ids, indent=indent,
1685
1464
if revision.diff is not None:
1686
1465
to_file.write(indent + 'diff:\n')
1688
1466
# Note: we explicitly don't indent the diff (relative to the
1689
1467
# revision information) so that the output can be fed to patch -p0
1690
1468
self.show_diff(self.to_exact_file, revision.diff, indent)
1691
self.to_exact_file.flush()
1693
1470
def get_advice_separator(self):
1694
1471
"""Get the text separating the log from the closing advice."""
1794
1564
def log_revision(self, revision):
1795
1565
indent = ' ' * revision.merge_depth
1796
1566
self.to_file.write(self.log_string(revision.revno, revision.rev,
1797
self._max_chars, revision.tags, indent))
1567
self._max_chars, revision.tags, indent))
1798
1568
self.to_file.write('\n')
1800
1570
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1801
1571
"""Format log info into one string. Truncate tail of string
1803
:param revno: revision number or None.
1804
Revision numbers counts from 1.
1805
:param rev: revision object
1806
:param max_chars: maximum length of resulting string
1807
:param tags: list of tags or None
1808
:param prefix: string to prefix each line
1809
:return: formatted truncated string
1572
:param revno: revision number or None.
1573
Revision numbers counts from 1.
1574
:param rev: revision object
1575
:param max_chars: maximum length of resulting string
1576
:param tags: list of tags or None
1577
:param prefix: string to prefix each line
1578
:return: formatted truncated string
1813
1582
# show revno only when is not None
1814
1583
out.append("%s:" % revno)
1815
if max_chars is not None:
1816
out.append(self.truncate(
1817
self.short_author(rev), (max_chars + 3) // 4))
1819
out.append(self.short_author(rev))
1584
out.append(self.truncate(self.short_author(rev), 20))
1820
1585
out.append(self.date_string(rev))
1821
1586
if len(rev.parent_ids) > 1:
1822
1587
out.append('[merge]')
1824
tag_str = '{%s}' % (', '.join(sorted(tags)))
1589
tag_str = '{%s}' % (', '.join(tags))
1825
1590
out.append(tag_str)
1826
1591
out.append(rev.get_summary())
1827
1592
return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
2176
1917
tree1 = b.repository.revision_tree(rev1)
2178
1919
tree1 = b.repository.revision_tree(rev_id)
2179
kind = _get_kind_for_file(tree1, fp)
2180
info_list.append((fp, kind))
1920
file_id = tree1.path2id(fp)
1921
kind = _get_kind_for_file_id(tree1, file_id)
1922
info_list.append((fp, file_id, kind))
2181
1923
return b, info_list, start_rev_info, end_rev_info
2184
def _get_kind_for_file(tree, path):
2185
"""Return the kind of a path or None if it doesn't exist."""
2186
with tree.lock_read():
2188
return tree.stored_kind(path)
2189
except errors.NoSuchFile:
1926
def _get_kind_for_file_id(tree, file_id):
1927
"""Return the kind of a file-id or None if it doesn't exist."""
1928
if file_id is not None:
1929
return tree.kind(file_id)
2193
1934
properties_handler_registry = registry.Registry()
2195
# Use the properties handlers to print out bug information if available
2198
def _bugs_properties_handler(revision):
2200
related_bug_urls = []
2201
for bug_url, status in revision.iter_bugs():
2202
if status == 'fixed':
2203
fixed_bug_urls.append(bug_url)
2204
elif status == 'related':
2205
related_bug_urls.append(bug_url)
2208
text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
2209
ret[text] = ' '.join(fixed_bug_urls)
2210
if related_bug_urls:
2211
text = ngettext('related bug', 'related bugs',
2212
len(related_bug_urls))
2213
ret[text] = ' '.join(related_bug_urls)
2217
properties_handler_registry.register('bugs_properties_handler',
2218
_bugs_properties_handler)
2221
1937
# adapters which revision ids to log are filtered. When log is called, the
2222
1938
# log_rev_iterator is adapted through each of these factory methods.