105
101
TODO: Perhaps some way to limit this to only particular revisions,
106
102
or to traverse a non-mainline set of revisions?
111
for revision_id in branch.revision_history():
112
this_inv = branch.repository.get_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
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)
119
113
# now we know how it was last time, and how it is in this revision.
120
114
# are those two states effectively the same or not?
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
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
130
120
elif this_path != last_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
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
128
last_verifier = this_verifier
137
129
last_path = this_path
141
def _enumerate_history(branch):
144
for rev_id in branch.revision_history():
145
rh.append((revno, rev_id))
130
last_tree = this_tree
131
if last_path is None:
150
136
def show_log(branch,
152
specific_fileid=None,
154
139
direction='reverse',
155
140
start_revision=None,
156
141
end_revision=None,
160
145
"""Write out human-readable log of commits to this branch.
162
147
This function is being retained for backwards compatibility but
179
161
:param end_revision: If not None, only show revisions <= end_revision
181
:param search: If not None, only show revisions with matching commit
184
163
:param limit: If set, shows only 'limit' revisions, all revisions are shown
187
166
:param show_diff: If True, output a diff after each revision.
168
: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'
200
174
delta_type = None
203
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)
209
195
# Build the request and execute it
210
rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
196
rqst = make_log_request_dict(
211
198
start_revision=start_revision, end_revision=end_revision,
212
limit=limit, message_search=search,
213
delta_type=delta_type, diff_type=diff_type)
199
limit=limit, delta_type=delta_type, diff_type=diff_type)
214
200
Logger(branch, rqst).show(lf)
217
# Note: This needs to be kept this in sync with the defaults in
203
# Note: This needs to be kept in sync with the defaults in
218
204
# make_log_request_dict() below
219
205
_DEFAULT_REQUEST_PARAMS = {
220
206
'direction': 'reverse',
222
208
'generate_tags': True,
223
209
'exclude_common_ancestry': False,
224
210
'_match_using_deltas': True,
228
def make_log_request_dict(direction='reverse', specific_fileids=None,
214
def make_log_request_dict(direction='reverse', specific_files=None,
229
215
start_revision=None, end_revision=None, limit=None,
230
message_search=None, levels=1, generate_tags=True,
216
message_search=None, levels=None, generate_tags=True,
232
218
diff_type=None, _match_using_deltas=True,
233
exclude_common_ancestry=False,
219
exclude_common_ancestry=False, match=None,
220
signature=False, omit_merges=False,
235
222
"""Convenience function for making a logging request dictionary.
257
244
matching commit messages
259
246
:param levels: the number of levels of revisions to
260
generate; 1 for just the mainline; 0 for all levels.
247
generate; 1 for just the mainline; 0 for all levels, or None for
262
250
:param generate_tags: If True, include tags for matched revisions.
264
252
:param delta_type: Either 'full', 'partial' or None.
265
253
'full' means generate the complete delta - adds/deletes/modifies/etc;
266
'partial' means filter the delta using specific_fileids;
254
'partial' means filter the delta using specific_files;
267
255
None means do not generate any delta.
269
257
:param diff_type: Either 'full', 'partial' or None.
270
258
'full' means generate the complete diff - adds/deletes/modifies/etc;
271
'partial' means filter the diff using specific_fileids;
259
'partial' means filter the diff using specific_files;
272
260
None means do not generate any diff.
274
262
:param _match_using_deltas: a private parameter controlling the
275
algorithm used for matching specific_fileids. This parameter
276
may be removed in the future so bzrlib client code should NOT
263
algorithm used for matching specific_files. This parameter
264
may be removed in the future so breezy client code should NOT
279
267
:param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
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]}
283
290
'direction': direction,
284
'specific_fileids': specific_fileids,
291
'specific_files': specific_files,
285
292
'start_revision': start_revision,
286
293
'end_revision': end_revision,
288
'message_search': message_search,
289
295
'levels': levels,
290
296
'generate_tags': generate_tags,
291
297
'delta_type': delta_type,
292
298
'diff_type': diff_type,
293
299
'exclude_common_ancestry': exclude_common_ancestry,
300
'signature': signature,
302
'omit_merges': omit_merges,
294
303
# Add 'private' attributes for features that may be deprecated
295
304
'_match_using_deltas': _match_using_deltas,
354
381
# Tweak the LogRequest based on what the LogFormatter can handle.
355
382
# (There's no point generating stuff if the formatter can't display it.)
357
rqst['levels'] = lf.get_levels()
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()
358
388
if not getattr(lf, 'supports_tags', False):
359
389
rqst['generate_tags'] = False
360
390
if not getattr(lf, 'supports_delta', False):
361
391
rqst['delta_type'] = None
362
392
if not getattr(lf, 'supports_diff', False):
363
393
rqst['diff_type'] = None
394
if not getattr(lf, 'supports_signatures', False):
395
rqst['signature'] = False
365
397
# Find and print the interesting revisions
366
398
generator = self._generator_factory(self.branch, rqst)
367
for lr in generator.iter_log_revisions():
400
for lr in generator.iter_log_revisions():
402
except errors.GhostRevisionUnusableHere:
403
raise errors.CommandError(
404
gettext('Further revision history missing.'))
371
407
def _generator_factory(self, branch, rqst):
372
408
"""Make the LogGenerator object to use.
374
410
Subclasses may wish to override this.
376
return _DefaultLogGenerator(branch, rqst)
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)
379
481
class _StartNotLinearAncestor(Exception):
383
485
class _DefaultLogGenerator(LogGenerator):
384
486
"""The default generator of log revisions."""
386
def __init__(self, branch, rqst):
489
self, branch, levels=None, limit=None, diff_type=None,
490
delta_type=None, show_signature=None, omit_merges=None,
491
generate_tags=None, specific_files=None, match=None,
492
start_revision=None, end_revision=None, direction=None,
493
exclude_common_ancestry=None, _match_using_deltas=None,
387
495
self.branch = branch
389
if rqst.get('generate_tags') and branch.supports_tags():
498
self.diff_type = diff_type
499
self.delta_type = delta_type
500
self.show_signature = signature
501
self.omit_merges = omit_merges
502
self.specific_files = specific_files
504
self.start_revision = start_revision
505
self.end_revision = end_revision
506
self.direction = direction
507
self.exclude_common_ancestry = exclude_common_ancestry
508
self._match_using_deltas = _match_using_deltas
509
if generate_tags and branch.supports_tags():
390
510
self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
392
512
self.rev_tag_dict = {}
397
517
:return: An iterator yielding LogRevision objects.
400
levels = rqst.get('levels')
401
limit = rqst.get('limit')
402
diff_type = rqst.get('diff_type')
404
520
revision_iterator = self._create_log_revision_iterator()
405
521
for revs in revision_iterator:
406
522
for (rev_id, revno, merge_depth), rev, delta in revs:
407
523
# 0 levels means show everything; merge_depth counts from 0
408
if levels != 0 and merge_depth >= levels:
410
if diff_type is None:
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:
413
diff = self._format_diff(rev, rev_id, diff_type)
414
yield LogRevision(rev, revno, merge_depth, delta,
415
self.rev_tag_dict.get(rev_id), diff)
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)
418
if log_count >= limit:
546
if log_count >= self.limit:
421
def _format_diff(self, rev, rev_id, diff_type):
422
repo = self.branch.repository
423
if len(rev.parent_ids) == 0:
424
ancestor_id = _mod_revision.NULL_REVISION
426
ancestor_id = rev.parent_ids[0]
427
tree_1 = repo.revision_tree(ancestor_id)
428
tree_2 = repo.revision_tree(rev_id)
429
file_ids = self.rqst.get('specific_fileids')
430
if diff_type == 'partial' and file_ids is not None:
431
specific_files = [tree_2.id2path(id) for id in file_ids]
433
specific_files = None
435
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
439
549
def _create_log_revision_iterator(self):
440
550
"""Create a revision iterator for log.
442
552
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
445
self.start_rev_id, self.end_rev_id = _get_revision_limits(
446
self.branch, self.rqst.get('start_revision'),
447
self.rqst.get('end_revision'))
448
if self.rqst.get('_match_using_deltas'):
449
return self._log_revision_iterator_using_delta_matching()
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,
451
569
# We're using the per-file-graph algorithm. This scales really
452
570
# well but only makes sense if there is a single file and it's
453
571
# not a directory
454
file_count = len(self.rqst.get('specific_fileids'))
572
file_count = len(self.specific_files)
455
573
if file_count != 1:
456
raise BzrError("illegal LogRequest: must match-using-deltas "
574
raise errors.BzrError(
575
"illegal LogRequest: must match-using-deltas "
457
576
"when logging %d files" % file_count)
458
return self._log_revision_iterator_using_per_file_graph()
460
def _log_revision_iterator_using_delta_matching(self):
461
# Get the base revisions, filtering by the revision range
463
generate_merge_revisions = rqst.get('levels') != 1
464
delayed_graph_generation = not rqst.get('specific_fileids') and (
465
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
466
view_revisions = _calc_view_revisions(
467
self.branch, self.start_rev_id, self.end_rev_id,
468
rqst.get('direction'),
469
generate_merge_revisions=generate_merge_revisions,
470
delayed_graph_generation=delayed_graph_generation,
471
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
473
# Apply the other filters
474
return make_log_rev_iterator(self.branch, view_revisions,
475
rqst.get('delta_type'), rqst.get('message_search'),
476
file_ids=rqst.get('specific_fileids'),
477
direction=rqst.get('direction'))
479
def _log_revision_iterator_using_per_file_graph(self):
480
# Get the base revisions, filtering by the revision range.
481
# Note that we always generate the merge revisions because
482
# filter_revisions_touching_file_id() requires them ...
484
view_revisions = _calc_view_revisions(
485
self.branch, self.start_rev_id, self.end_rev_id,
486
rqst.get('direction'), generate_merge_revisions=True,
487
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
488
if not isinstance(view_revisions, list):
489
view_revisions = list(view_revisions)
490
view_revisions = _filter_revisions_touching_file_id(self.branch,
491
rqst.get('specific_fileids')[0], view_revisions,
492
include_merges=rqst.get('levels') != 1)
493
return make_log_rev_iterator(self.branch, view_revisions,
494
rqst.get('delta_type'), rqst.get('message_search'))
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
497
589
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
597
a list of the same tuples.
507
599
if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
raise errors.BzrCommandError(
509
'--exclude-common-ancestry requires two different revisions')
600
raise errors.CommandError(gettext(
601
'--exclude-common-ancestry requires two different revisions'))
510
602
if direction not in ('reverse', 'forward'):
511
raise ValueError('invalid direction %r' % direction)
512
br_revno, br_rev_id = branch.last_revision_info()
603
raise ValueError(gettext('invalid direction %r') % direction)
604
br_rev_id = branch.last_revision()
605
if br_rev_id == _mod_revision.NULL_REVISION:
516
608
if (end_rev_id and start_rev_id == end_rev_id
517
609
and (not generate_merge_revisions
518
610
or not _has_merges(branch, end_rev_id))):
519
611
# If a single revision is requested, check we can handle it
520
iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
522
elif not generate_merge_revisions:
523
# If we only want to see linear revisions, we can iterate ...
524
iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
526
if direction == 'forward':
527
iter_revs = reversed(iter_revs)
529
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
530
direction, delayed_graph_generation,
531
exclude_common_ancestry)
532
if direction == 'forward':
533
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
612
return _generate_one_revision(branch, end_rev_id, br_rev_id,
614
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)))
540
644
return [(br_rev_id, br_revno, 0)]
542
revno = branch.revision_id_to_dotted_revno(rev_id)
543
revno_str = '.'.join(str(n) for n in revno)
646
revno_str = _compute_revno_str(branch, rev_id)
544
647
return [(rev_id, revno_str, 0)]
547
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
548
result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
549
# If a start limit was given and it's not obviously an
550
# ancestor of the end limit, check it before outputting anything
551
if direction == 'forward' or (start_rev_id
552
and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
554
result = list(result)
555
except _StartNotLinearAncestor:
556
raise errors.BzrCommandError('Start revision not found in'
557
' left-hand history of end revision.')
561
650
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
651
delayed_graph_generation,
563
652
exclude_common_ancestry=False):
623
713
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)
626
731
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
732
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
628
733
if start_rev_id and end_rev_id:
629
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
630
end_dotted = branch.revision_id_to_dotted_revno(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
631
740
if len(start_dotted) == 1 and len(end_dotted) == 1:
632
741
# both on mainline
633
742
return start_dotted[0] <= end_dotted[0]
634
743
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
635
start_dotted[0:1] == end_dotted[0:1]):
744
start_dotted[0:1] == end_dotted[0:1]):
636
745
# both on same development line
637
746
return start_dotted[2] <= end_dotted[2]
646
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
755
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
756
exclude_common_ancestry=False):
647
757
"""Calculate a sequence of revisions to view, newest to oldest.
649
759
:param start_rev_id: the lower revision-id
650
760
: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.
651
763
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
764
dotted_revno will be None for ghosts
652
765
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
is not found walking the left-hand history
766
is not found walking the left-hand history
655
br_revno, br_rev_id = branch.last_revision_info()
656
768
repo = branch.repository
769
graph = repo.get_graph()
657
770
if start_rev_id is None and end_rev_id is None:
659
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
660
yield revision_id, str(cur_revno), 0
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:
800
br_rev_id = branch.last_revision()
663
801
if end_rev_id is None:
664
802
end_rev_id = br_rev_id
665
803
found_start = start_rev_id is None
666
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
667
revno = branch.revision_id_to_dotted_revno(revision_id)
668
revno_str = '.'.join(str(n) for n in revno)
669
if not found_start and revision_id == start_rev_id:
670
yield revision_id, revno_str, 0
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
674
yield revision_id, revno_str, 0
677
raise _StartNotLinearAncestor()
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()
680
828
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
721
869
yield rev_id, '.'.join(map(str, revno)), merge_depth
724
@deprecated_function(deprecated_in((2, 2, 0)))
725
def calculate_view_revisions(branch, start_revision, end_revision, direction,
726
specific_fileid, generate_merge_revisions):
727
"""Calculate the revisions to view.
729
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
730
a list of the same tuples.
732
start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
734
view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
735
direction, generate_merge_revisions or specific_fileid))
737
view_revisions = _filter_revisions_touching_file_id(branch,
738
specific_fileid, view_revisions,
739
include_merges=generate_merge_revisions)
740
return _rebase_merge_depth(view_revisions)
743
872
def _rebase_merge_depth(view_revisions):
744
873
"""Adjust depths upwards so the top level is 0."""
745
874
# If either the first or last revision have a merge_depth of 0, we're done
746
875
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
747
min_depth = min([d for r,n,d in view_revisions])
876
min_depth = min([d for r, n, d in view_revisions])
748
877
if min_depth != 0:
749
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
878
view_revisions = [(r, n, d - min_depth)
879
for r, n, d in view_revisions]
750
880
return view_revisions
753
883
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
754
file_ids=None, direction='reverse'):
884
files=None, direction='reverse'):
755
885
"""Create a revision iterator for log.
757
887
:param branch: The branch being logged.
759
889
:param generate_delta: Whether to generate a delta for each revision.
760
890
Permitted values are None, 'full' and 'partial'.
761
891
:param search: A user text search string.
762
:param file_ids: If non empty, only revisions matching one or more of
763
the file-ids are to be kept.
892
:param files: If non empty, only revisions matching one or more of
893
the files are to be kept.
764
894
:param direction: the direction in which view_revisions is sorted
765
895
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
768
898
# Convert view_revisions into (view, None, None) groups to fit with
769
899
# the standard interface here.
770
if type(view_revisions) == list:
900
if isinstance(view_revisions, list):
771
901
# A single batch conversion is faster than many incremental ones.
772
902
# As we have all the data, do a batch conversion.
773
903
nones = [None] * len(view_revisions)
774
log_rev_iterator = iter([zip(view_revisions, nones, nones)])
904
log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
777
907
for view in view_revisions:
781
911
# It would be nicer if log adapters were first class objects
782
912
# with custom parameters. This will do for now. IGC 20090127
783
913
if adapter == _make_delta_filter:
784
log_rev_iterator = adapter(branch, generate_delta,
785
search, log_rev_iterator, file_ids, direction)
914
log_rev_iterator = adapter(
915
branch, generate_delta, search, log_rev_iterator, files,
787
log_rev_iterator = adapter(branch, generate_delta,
788
search, log_rev_iterator)
918
log_rev_iterator = adapter(
919
branch, generate_delta, search, log_rev_iterator)
789
920
return log_rev_iterator
792
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
923
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
793
924
"""Create a filtered iterator of log_rev_iterator matching on a regex.
795
926
:param branch: The branch being logged.
796
927
:param generate_delta: Whether to generate a delta for each revision.
797
:param search: A user text search string.
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
798
932
:param log_rev_iterator: An input iterator containing all revisions that
799
933
could be displayed, in lists.
800
934
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
804
938
return log_rev_iterator
805
searchRE = re_compile_checked(search, re.IGNORECASE,
806
'log message filter')
807
return _filter_message_re(searchRE, log_rev_iterator)
810
def _filter_message_re(searchRE, 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):
811
946
for revs in log_rev_iterator:
813
for (rev_id, revno, merge_depth), rev, delta in revs:
814
if searchRE.search(rev.message):
815
new_revs.append(((rev_id, revno, merge_depth), rev, delta))
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)
819
971
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
820
fileids=None, direction='reverse'):
972
files=None, direction='reverse'):
821
973
"""Add revision deltas to a log iterator if needed.
823
975
:param branch: The branch being logged.
826
978
:param search: A user text search string.
827
979
:param log_rev_iterator: An input iterator containing all revisions that
828
980
could be displayed, in lists.
829
:param fileids: If non empty, only revisions matching one or more of
830
the file-ids are to be kept.
981
:param files: If non empty, only revisions matching one or more of
982
the files are to be kept.
831
983
:param direction: the direction in which view_revisions is sorted
832
984
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
835
if not generate_delta and not fileids:
987
if not generate_delta and not files:
836
988
return log_rev_iterator
837
989
return _generate_deltas(branch.repository, log_rev_iterator,
838
generate_delta, fileids, direction)
841
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
990
generate_delta, files, direction)
993
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
843
995
"""Create deltas for each batch of revisions in log_rev_iterator.
845
997
If we're only generating deltas for the sake of filtering against
846
file-ids, we stop generating deltas once all file-ids reach the
998
files, we stop generating deltas once all files reach the
847
999
appropriate life-cycle point. If we're receiving data newest to
848
1000
oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
850
check_fileids = fileids is not None and len(fileids) > 0
852
fileid_set = set(fileids)
1002
check_files = files is not None and len(files) > 0
1004
file_set = set(files)
853
1005
if direction == 'reverse':
856
1008
stop_on = 'remove'
859
1011
for revs in log_rev_iterator:
860
# If we were matching against fileids and we've run out,
1012
# If we were matching against files and we've run out,
861
1013
# there's nothing left to do
862
if check_fileids and not fileid_set:
1014
if check_files and not file_set:
864
1016
revisions = [rev[1] for rev in revs]
866
if delta_type == 'full' and not check_fileids:
867
deltas = repository.get_deltas_for_revisions(revisions)
868
for rev, delta in izip(revs, deltas):
1018
if delta_type == 'full' and not check_files:
1019
deltas = repository.get_revision_deltas(revisions)
1020
for rev, delta in zip(revs, deltas):
869
1021
new_revs.append((rev[0], rev[1], delta))
871
deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
872
for rev, delta in izip(revs, deltas):
1023
deltas = repository.get_revision_deltas(
1024
revisions, specific_files=file_set)
1025
for rev, delta in zip(revs, deltas):
874
1027
if delta is None or not delta.has_changed():
877
_update_fileids(delta, fileid_set, stop_on)
1030
_update_files(delta, file_set, stop_on)
878
1031
if delta_type is None:
880
1033
elif delta_type == 'full':
894
def _update_fileids(delta, fileids, stop_on):
895
"""Update the set of file-ids to search based on file lifecycle events.
897
:param fileids: a set of fileids to update
898
:param stop_on: either 'add' or 'remove' - take file-ids out of the
899
fileids set once their add or remove entry is detected respectively
1047
def _update_files(delta, files, stop_on):
1048
"""Update the set of files to search based on file lifecycle events.
1050
:param files: a set of files to update
1051
:param stop_on: either 'add' or 'remove' - take files out of the
1052
files set once their add or remove entry is detected respectively
901
1054
if stop_on == 'add':
902
1055
for item in delta.added:
903
if item[1] in fileids:
904
fileids.remove(item[1])
1056
if item.path[1] in files:
1057
files.remove(item.path[1])
1058
for item in delta.copied + delta.renamed:
1059
if item.path[1] in files:
1060
files.remove(item.path[1])
1061
files.add(item.path[0])
1062
if item.kind[1] == 'directory':
1063
for path in list(files):
1064
if is_inside(item.path[1], path):
1066
files.add(item.path[0] + path[len(item.path[1]):])
905
1067
elif stop_on == 'delete':
906
1068
for item in delta.removed:
907
if item[1] in fileids:
908
fileids.remove(item[1])
1069
if item.path[0] in files:
1070
files.remove(item.path[0])
1071
for item in delta.copied + delta.renamed:
1072
if item.path[0] in files:
1073
files.remove(item.path[0])
1074
files.add(item.path[1])
1075
if item.kind[0] == 'directory':
1076
for path in list(files):
1077
if is_inside(item.path[0], path):
1079
files.add(item.path[1] + path[len(item.path[0]):])
911
1082
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
955
1123
def _get_revision_limits(branch, start_revision, end_revision):
956
1124
"""Get and check revision limits.
958
:param branch: The branch containing the revisions.
960
:param start_revision: The first revision to be logged.
961
For backwards compatibility this may be a mainline integer revno,
962
but for merge revision support a RevisionInfo is expected.
964
:param end_revision: The last revision to be logged.
965
For backwards compatibility this may be a mainline integer revno,
966
but for merge revision support a RevisionInfo is expected.
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
968
1132
:return: (start_rev_id, end_rev_id) tuple.
970
branch_revno, branch_rev_id = branch.last_revision_info()
971
1134
start_rev_id = None
972
if start_revision is 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:
975
if isinstance(start_revision, revisionspec.RevisionInfo):
976
start_rev_id = start_revision.rev_id
977
start_revno = start_revision.revno or 1
979
branch.check_real_revno(start_revision)
980
start_revno = start_revision
981
start_rev_id = branch.get_rev_id(start_revno)
983
1144
end_rev_id = None
984
if end_revision is None:
985
end_revno = branch_revno
987
if isinstance(end_revision, revisionspec.RevisionInfo):
988
end_rev_id = end_revision.rev_id
989
end_revno = end_revision.revno or branch_revno
991
branch.check_real_revno(end_revision)
992
end_revno = end_revision
993
end_rev_id = branch.get_rev_id(end_revno)
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
995
if branch_revno != 0:
1152
if branch.last_revision() != _mod_revision.NULL_REVISION:
996
1153
if (start_rev_id == _mod_revision.NULL_REVISION
997
or end_rev_id == _mod_revision.NULL_REVISION):
998
raise errors.BzrCommandError('Logging revision 0 is invalid.')
999
if start_revno > end_revno:
1000
raise errors.BzrCommandError("Start revision must be older than "
1001
"the end 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."))
1002
1160
return (start_rev_id, end_rev_id)
1084
1243
return mainline_revs, rev_nos, start_rev_id, end_rev_id
1087
@deprecated_function(deprecated_in((2, 2, 0)))
1088
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1089
"""Filter view_revisions based on revision ranges.
1091
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1092
tuples to be filtered.
1094
:param start_rev_id: If not NONE specifies the first revision to be logged.
1095
If NONE then all revisions up to the end_rev_id are logged.
1097
:param end_rev_id: If not NONE specifies the last revision to be logged.
1098
If NONE then all revisions up to the end of the log are logged.
1100
:return: The filtered view_revisions.
1102
if start_rev_id or end_rev_id:
1103
revision_ids = [r for r, n, d in view_revisions]
1105
start_index = revision_ids.index(start_rev_id)
1108
if start_rev_id == end_rev_id:
1109
end_index = start_index
1112
end_index = revision_ids.index(end_rev_id)
1114
end_index = len(view_revisions) - 1
1115
# To include the revisions merged into the last revision,
1116
# extend end_rev_id down to, but not including, the next rev
1117
# with the same or lesser merge_depth
1118
end_merge_depth = view_revisions[end_index][2]
1120
for index in xrange(end_index+1, len(view_revisions)+1):
1121
if view_revisions[index][2] <= end_merge_depth:
1122
end_index = index - 1
1125
# if the search falls off the end then log to the end as well
1126
end_index = len(view_revisions) - 1
1127
view_revisions = view_revisions[start_index:end_index+1]
1128
return view_revisions
1131
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1132
include_merges=True):
1133
r"""Return the list of revision ids which touch a given file 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.
1135
1250
The function filters view_revisions and returns a subset.
1136
This includes the revisions which directly change the file id,
1251
This includes the revisions which directly change the path,
1137
1252
and the revisions which merge these changes. So if the
1138
1253
revision graph is::
1213
@deprecated_function(deprecated_in((2, 2, 0)))
1214
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1215
include_merges=True):
1216
"""Produce an iterator of revisions to show
1217
:return: an iterator of (revision_id, revno, merge_depth)
1218
(if there is no revno for a revision, None is supplied)
1220
if not include_merges:
1221
revision_ids = mainline_revs[1:]
1222
if direction == 'reverse':
1223
revision_ids.reverse()
1224
for revision_id in revision_ids:
1225
yield revision_id, str(rev_nos[revision_id]), 0
1227
graph = branch.repository.get_graph()
1228
# This asks for all mainline revisions, which means we only have to spider
1229
# sideways, rather than depth history. That said, its still size-of-history
1230
# and should be addressed.
1231
# mainline_revisions always includes an extra revision at the beginning, so
1233
parent_map = dict(((key, value) for key, value in
1234
graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1235
# filter out ghosts; merge_sort errors on ghosts.
1236
rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1237
merge_sorted_revisions = tsort.merge_sort(
1241
generate_revno=True)
1243
if direction == 'forward':
1244
# forward means oldest first.
1245
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1246
elif direction != 'reverse':
1247
raise ValueError('invalid direction %r' % direction)
1249
for (sequence, rev_id, merge_depth, revno, end_of_merge
1250
) in merge_sorted_revisions:
1251
yield rev_id, '.'.join(map(str, revno)), merge_depth
1254
1332
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1255
1333
"""Reverse revisions by depth.
1257
1335
Revisions with a different depth are sorted as a group with the previous
1258
revision of that depth. There may be no topological justification for this,
1336
revision of that depth. There may be no topological justification for this
1259
1337
but it looks much nicer.
1261
1339
# Add a fake revision at start so that we can always attach sub revisions
1312
1394
to indicate which LogRevision attributes it supports:
1314
1396
- supports_delta must be True if this log formatter supports delta.
1315
Otherwise the delta attribute may not be populated. The 'delta_format'
1316
attribute describes whether the 'short_status' format (1) or the long
1317
one (2) should be used.
1397
Otherwise the delta attribute may not be populated. The 'delta_format'
1398
attribute describes whether the 'short_status' format (1) or the long
1399
one (2) should be used.
1319
1401
- supports_merge_revisions must be True if this log formatter supports
1320
merge revisions. If not, then only mainline revisions will be passed
1402
merge revisions. If not, then only mainline revisions will be passed
1323
1405
- preferred_levels is the number of levels this formatter defaults to.
1324
The default value is zero meaning display all levels.
1325
This value is only relevant if supports_merge_revisions is True.
1406
The default value is zero meaning display all levels.
1407
This value is only relevant if supports_merge_revisions is True.
1327
1409
- supports_tags must be True if this log formatter supports tags.
1328
Otherwise the tags attribute may not be populated.
1410
Otherwise the tags attribute may not be populated.
1330
1412
- supports_diff must be True if this log formatter supports diffs.
1331
Otherwise the diff attribute may not be populated.
1413
Otherwise the diff attribute may not be populated.
1415
- supports_signatures must be True if this log formatter supports GPG
1333
1418
Plugins can register functions to show custom revision properties using
1334
1419
the properties_handler_registry. The registered function
1335
must respect the following interface description:
1420
must respect the following interface description::
1336
1422
def my_show_properties(properties_dict):
1337
1423
# code that returns a dict {'name':'value'} of the properties
1355
1441
let the log formatter decide.
1356
1442
:param show_advice: whether to show advice at the end of the
1444
:param author_list_handler: callable generating a list of
1445
authors to display for a given revision
1359
1447
self.to_file = to_file
1360
1448
# 'exact' stream used to show diff, it should print content 'as is'
1361
# and should not try to decode/encode it to unicode to avoid bug #328007
1449
# and should not try to decode/encode it to unicode to avoid bug
1362
1451
if to_exact_file is not None:
1363
1452
self.to_exact_file = to_exact_file
1365
# XXX: somewhat hacky; this assumes it's a codec writer; it's better
1366
# for code that expects to get diffs to pass in the exact file
1454
# XXX: somewhat hacky; this assumes it's a codec writer; it's
1455
# better for code that expects to get diffs to pass in the exact
1368
1457
self.to_exact_file = getattr(to_file, 'stream', to_file)
1369
1458
self.show_ids = show_ids
1370
1459
self.show_timezone = show_timezone
1371
1460
if delta_format is None:
1372
1461
# Ensures backward compatibility
1373
delta_format = 2 # long format
1462
delta_format = 2 # long format
1374
1463
self.delta_format = delta_format
1375
1464
self.levels = levels
1376
1465
self._show_advice = show_advice
1377
1466
self._merge_count = 0
1467
self._author_list_handler = author_list_handler
1379
1469
def get_levels(self):
1380
1470
"""Get the number of levels to display or 0 for all."""
1414
1504
def short_author(self, rev):
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1505
return self.authors(rev, 'first', short=True, sep=', ')
1507
def authors(self, rev, who, short=False, sep=None):
1508
"""Generate list of authors, taking --authors option into account.
1510
The caller has to specify the name of a author list handler,
1511
as provided by the author list registry, using the ``who``
1512
argument. That name only sets a default, though: when the
1513
user selected a different author list generation using the
1514
``--authors`` command line switch, as represented by the
1515
``author_list_handler`` constructor argument, that value takes
1518
:param rev: The revision for which to generate the list of authors.
1519
:param who: Name of the default handler.
1520
:param short: Whether to shorten names to either name or address.
1521
:param sep: What separator to use for automatic concatenation.
1523
if self._author_list_handler is not None:
1524
# The user did specify --authors, which overrides the default
1525
author_list_handler = self._author_list_handler
1527
# The user didn't specify --authors, so we use the caller's default
1528
author_list_handler = author_list_registry.get(who)
1529
names = author_list_handler(rev)
1531
for i in range(len(names)):
1532
name, address = config.parse_username(names[i])
1538
names = sep.join(names)
1420
1541
def merge_marker(self, revision):
1421
1542
"""Get the merge marker to include in the output or '' if none."""
1513
1640
lines = [_LONG_SEP]
1514
1641
if revision.revno is not None:
1515
1642
lines.append('revno: %s%s' % (revision.revno,
1516
self.merge_marker(revision)))
1643
self.merge_marker(revision)))
1517
1644
if revision.tags:
1518
lines.append('tags: %s' % (', '.join(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'),))
1519
1649
if self.show_ids:
1520
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1521
1650
for parent_id in revision.rev.parent_ids:
1522
lines.append('parent: %s' % (parent_id,))
1651
lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1523
1652
lines.extend(self.custom_properties(revision.rev))
1525
1654
committer = revision.rev.committer
1526
authors = revision.rev.get_apparent_authors()
1655
authors = self.authors(revision.rev, 'all')
1527
1656
if authors != [committer]:
1528
1657
lines.append('author: %s' % (", ".join(authors),))
1529
1658
lines.append('committer: %s' % (committer,))
1598
1730
to_file = self.to_file
1600
1732
if revision.tags:
1601
tags = ' {%s}' % (', '.join(revision.tags))
1733
tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1602
1734
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
revision.revno, self.short_author(revision.rev),
1604
format_date(revision.rev.timestamp,
1605
revision.rev.timezone or 0,
1606
self.show_timezone, date_fmt="%Y-%m-%d",
1608
tags, self.merge_marker(revision)))
1609
self.show_properties(revision.rev, indent+offset)
1735
revision.revno or "", self.short_author(
1737
format_date(revision.rev.timestamp,
1738
revision.rev.timezone or 0,
1739
self.show_timezone, date_fmt="%Y-%m-%d",
1741
tags, self.merge_marker(revision)))
1742
self.show_properties(revision.rev, indent + offset)
1743
if self.show_ids or revision.revno is None:
1611
1744
to_file.write(indent + offset + 'revision-id:%s\n'
1612
% (revision.rev.revision_id,))
1745
% (revision.rev.revision_id.decode('utf-8'),))
1613
1746
if not revision.rev.message:
1614
1747
to_file.write(indent + offset + '(no message)\n')
1661
1794
def log_revision(self, revision):
1662
1795
indent = ' ' * revision.merge_depth
1663
1796
self.to_file.write(self.log_string(revision.revno, revision.rev,
1664
self._max_chars, revision.tags, indent))
1797
self._max_chars, revision.tags, indent))
1665
1798
self.to_file.write('\n')
1667
1800
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1801
"""Format log info into one string. Truncate tail of string
1669
:param revno: revision number or None.
1670
Revision numbers counts from 1.
1671
:param rev: revision object
1672
:param max_chars: maximum length of resulting string
1673
:param tags: list of tags or None
1674
:param prefix: string to prefix each line
1675
:return: formatted truncated 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
1679
1813
# show revno only when is not None
1680
1814
out.append("%s:" % revno)
1681
out.append(self.truncate(self.short_author(rev), 20))
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))
1682
1820
out.append(self.date_string(rev))
1683
1821
if len(rev.parent_ids) > 1:
1684
1822
out.append('[merge]')
1686
tag_str = '{%s}' % (', '.join(tags))
1824
tag_str = '{%s}' % (', '.join(sorted(tags)))
1687
1825
out.append(tag_str)
1688
1826
out.append(rev.get_summary())
1689
1827
return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1772
1914
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1773
1915
except KeyError:
1774
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1777
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
# deprecated; for compatibility
1779
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1780
lf.show(revno, rev, delta)
1916
raise errors.CommandError(
1917
gettext("unknown log formatter: %r") % name)
1920
def author_list_all(rev):
1921
return rev.get_apparent_authors()[:]
1924
def author_list_first(rev):
1925
lst = rev.get_apparent_authors()
1932
def author_list_committer(rev):
1933
return [rev.committer]
1936
author_list_registry = registry.Registry()
1938
author_list_registry.register('all', author_list_all,
1941
author_list_registry.register('first', author_list_first,
1944
author_list_registry.register('committer', author_list_committer,
1783
1948
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1800
1965
# This is the first index which is different between
1802
1967
base_idx = None
1803
for i in xrange(max(len(new_rh),
1968
for i in range(max(len(new_rh), len(old_rh))):
1805
1969
if (len(new_rh) <= i
1806
1970
or len(old_rh) <= i
1807
or new_rh[i] != old_rh[i]):
1971
or new_rh[i] != old_rh[i]):
1811
1975
if base_idx is None:
1812
1976
to_file.write('Nothing seems to have changed\n')
1814
## TODO: It might be nice to do something like show_log
1815
## and show the merged entries. But since this is the
1816
## removed revisions, it shouldn't be as important
1978
# TODO: It might be nice to do something like show_log
1979
# and show the merged entries. But since this is the
1980
# removed revisions, it shouldn't be as important
1817
1981
if base_idx < len(old_rh):
1818
to_file.write('*'*60)
1982
to_file.write('*' * 60)
1819
1983
to_file.write('\nRemoved Revisions:\n')
1820
1984
for i in range(base_idx, len(old_rh)):
1821
1985
rev = branch.repository.get_revision(old_rh[i])
1822
lr = LogRevision(rev, i+1, 0, None)
1986
lr = LogRevision(rev, i + 1, 0, None)
1823
1987
lf.log_revision(lr)
1824
to_file.write('*'*60)
1988
to_file.write('*' * 60)
1825
1989
to_file.write('\n\n')
1826
1990
if base_idx < len(new_rh):
1827
1991
to_file.write('Added Revisions:\n')
1828
1992
show_log(branch,
1832
1995
direction='forward',
1833
start_revision=base_idx+1,
1834
end_revision=len(new_rh),
1996
start_revision=base_idx + 1,
1997
end_revision=len(new_rh))
1838
2000
def get_history_change(old_revision_id, new_revision_id, repository):
1940
2102
:param file_list: the list of paths given on the command line;
1941
2103
the first of these can be a branch location or a file path,
1942
2104
the remainder must be file paths
2105
:param exit_stack: When the branch returned is read locked,
2106
an unlock call will be queued to the exit stack.
1943
2107
:return: (branch, info_list, start_rev_info, end_rev_info) where
1944
info_list is a list of (relative_path, file_id, kind) tuples where
2108
info_list is a list of (relative_path, found, kind) tuples where
1945
2109
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2110
branch will be read-locked.
1948
from builtins import _get_revision_range, safe_relpath_files
1949
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2112
from breezy.builtins import _get_revision_range
2113
tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2115
exit_stack.enter_context(b.lock_read())
1951
2116
# XXX: It's damn messy converting a list of paths to relative paths when
1952
2117
# those paths might be deleted ones, they might be on a case-insensitive
1953
2118
# filesystem and/or they might be in silly locations (like another branch).
1970
2135
tree = b.basis_tree()
1972
2137
for fp in relpaths:
1973
file_id = tree.path2id(fp)
1974
kind = _get_kind_for_file_id(tree, file_id)
2138
kind = _get_kind_for_file(tree, fp)
1976
2140
# go back to when time began
1977
2141
if tree1 is None:
1979
2143
rev1 = b.get_rev_id(1)
1980
2144
except errors.NoSuchRevision:
1981
2145
# No history at all
1985
2148
tree1 = b.repository.revision_tree(rev1)
1987
file_id = tree1.path2id(fp)
1988
kind = _get_kind_for_file_id(tree1, file_id)
1989
info_list.append((fp, file_id, kind))
2150
kind = _get_kind_for_file(tree1, fp)
2151
info_list.append((fp, kind))
1991
2153
elif start_rev_info == end_rev_info:
1992
2154
# One revision given - file must exist in it
1993
2155
tree = b.repository.revision_tree(end_rev_info.rev_id)
1994
2156
for fp in relpaths:
1995
file_id = tree.path2id(fp)
1996
kind = _get_kind_for_file_id(tree, file_id)
1997
info_list.append((fp, file_id, kind))
2157
kind = _get_kind_for_file(tree, fp)
2158
info_list.append((fp, kind))
2000
2161
# Revision range given. Get the file-id from the end tree.
2016
2176
tree1 = b.repository.revision_tree(rev1)
2018
2178
tree1 = b.repository.revision_tree(rev_id)
2019
file_id = tree1.path2id(fp)
2020
kind = _get_kind_for_file_id(tree1, file_id)
2021
info_list.append((fp, file_id, kind))
2179
kind = _get_kind_for_file(tree1, fp)
2180
info_list.append((fp, kind))
2022
2181
return b, info_list, start_rev_info, end_rev_info
2025
def _get_kind_for_file_id(tree, file_id):
2026
"""Return the kind of a file-id or None if it doesn't exist."""
2027
if file_id is not None:
2028
return tree.kind(file_id)
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:
2033
2193
properties_handler_registry = registry.Registry()
2035
2195
# Use the properties handlers to print out bug information if available
2036
2198
def _bugs_properties_handler(revision):
2037
if revision.properties.has_key('bugs'):
2038
bug_lines = revision.properties['bugs'].split('\n')
2039
bug_rows = [line.split(' ', 1) for line in bug_lines]
2040
fixed_bug_urls = [row[0] for row in bug_rows if
2041
len(row) > 1 and row[1] == 'fixed']
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)
2044
return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2047
2217
properties_handler_registry.register('bugs_properties_handler',
2048
2218
_bugs_properties_handler)