49
47
all the changes since the previous revision that touched hello.c.
50
from __future__ import absolute_import
53
from cStringIO import StringIO
54
from itertools import (
60
56
from warnings import (
64
from bzrlib.lazy_import import lazy_import
60
from .lazy_import import lazy_import
65
61
lazy_import(globals(), """
73
repository as _mod_repository,
74
69
revision as _mod_revision,
71
from breezy.i18n import gettext, ngettext
84
from bzrlib.osutils import (
80
from .osutils import (
86
82
format_date_with_offset_in_original_timezone,
83
get_diff_header_encoding,
87
84
get_terminal_encoding,
91
from bzrlib.symbol_versioning import (
97
def find_touching_revisions(branch, file_id):
98
"""Yield a description of revisions which affect the file_id.
92
from .tree import InterTree
95
def find_touching_revisions(repository, last_revision, last_tree, last_path):
96
"""Yield a description of revisions which affect the file.
100
98
Each returned element is (revno, revision_id, description)
105
103
TODO: Perhaps some way to limit this to only particular revisions,
106
104
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
106
last_verifier = last_tree.get_file_verifier(last_path)
107
graph = repository.get_graph()
108
history = list(graph.iter_lefthand_ancestry(last_revision, []))
110
for revision_id in history:
111
this_tree = repository.revision_tree(revision_id)
112
this_intertree = InterTree.get(this_tree, last_tree)
113
this_path = this_intertree.find_source_path(last_path)
119
115
# now we know how it was last time, and how it is in this revision.
120
116
# 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
117
if this_path is not None and last_path is None:
118
yield revno, revision_id, "deleted " + this_path
119
this_verifier = this_tree.get_file_verifier(this_path)
120
elif this_path is None and last_path is not None:
121
yield revno, revision_id, "added " + last_path
130
122
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
123
yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
124
this_verifier = this_tree.get_file_verifier(this_path)
126
this_verifier = this_tree.get_file_verifier(this_path)
127
if (this_verifier != last_verifier):
128
yield revno, revision_id, "modified " + this_path
130
last_verifier = this_verifier
137
131
last_path = this_path
141
def _enumerate_history(branch):
144
for rev_id in branch.revision_history():
145
rh.append((revno, rev_id))
132
last_tree = this_tree
133
if last_path is None:
150
138
def show_log(branch,
152
specific_fileid=None,
154
141
direction='reverse',
155
142
start_revision=None,
156
143
end_revision=None,
160
147
"""Write out human-readable log of commits to this branch.
162
149
This function is being retained for backwards compatibility but
179
163
:param end_revision: If not None, only show revisions <= end_revision
181
:param search: If not None, only show revisions with matching commit
184
165
:param limit: If set, shows only 'limit' revisions, all revisions are shown
187
168
:param show_diff: If True, output a diff after each revision.
170
: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
176
delta_type = None
203
diff_type = 'partial'
182
if isinstance(start_revision, int):
184
start_revision = revisionspec.RevisionInfo(branch, start_revision)
185
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
186
raise errors.InvalidRevisionNumber(start_revision)
188
if isinstance(end_revision, int):
190
end_revision = revisionspec.RevisionInfo(branch, end_revision)
191
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
192
raise errors.InvalidRevisionNumber(end_revision)
194
if end_revision is not None and end_revision.revno == 0:
195
raise errors.InvalidRevisionNumber(end_revision.revno)
209
197
# Build the request and execute it
210
rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
198
rqst = make_log_request_dict(
211
200
start_revision=start_revision, end_revision=end_revision,
212
limit=limit, message_search=search,
213
delta_type=delta_type, diff_type=diff_type)
201
limit=limit, delta_type=delta_type, diff_type=diff_type)
214
202
Logger(branch, rqst).show(lf)
217
# Note: This needs to be kept this in sync with the defaults in
205
# Note: This needs to be kept in sync with the defaults in
218
206
# make_log_request_dict() below
219
207
_DEFAULT_REQUEST_PARAMS = {
220
208
'direction': 'reverse',
222
210
'generate_tags': True,
223
211
'exclude_common_ancestry': False,
224
212
'_match_using_deltas': True,
228
def make_log_request_dict(direction='reverse', specific_fileids=None,
216
def make_log_request_dict(direction='reverse', specific_files=None,
229
217
start_revision=None, end_revision=None, limit=None,
230
message_search=None, levels=1, generate_tags=True,
218
message_search=None, levels=None, generate_tags=True,
232
220
diff_type=None, _match_using_deltas=True,
233
exclude_common_ancestry=False,
221
exclude_common_ancestry=False, match=None,
222
signature=False, omit_merges=False,
235
224
"""Convenience function for making a logging request dictionary.
257
246
matching commit messages
259
248
:param levels: the number of levels of revisions to
260
generate; 1 for just the mainline; 0 for all levels.
249
generate; 1 for just the mainline; 0 for all levels, or None for
262
252
:param generate_tags: If True, include tags for matched revisions.
264
254
:param delta_type: Either 'full', 'partial' or None.
265
255
'full' means generate the complete delta - adds/deletes/modifies/etc;
266
'partial' means filter the delta using specific_fileids;
256
'partial' means filter the delta using specific_files;
267
257
None means do not generate any delta.
269
259
:param diff_type: Either 'full', 'partial' or None.
270
260
'full' means generate the complete diff - adds/deletes/modifies/etc;
271
'partial' means filter the diff using specific_fileids;
261
'partial' means filter the diff using specific_files;
272
262
None means do not generate any diff.
274
264
: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
265
algorithm used for matching specific_files. This parameter
266
may be removed in the future so breezy client code should NOT
279
269
:param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
270
range operator or as a graph difference.
272
:param signature: show digital signature information
274
:param match: Dictionary of list of search strings to use when filtering
275
revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
276
the empty string to match any of the preceding properties.
278
:param omit_merges: If True, commits with more than one parent are
282
# Take care of old style message_search parameter
285
if 'message' in match:
286
match['message'].append(message_search)
288
match['message'] = [message_search]
290
match = {'message': [message_search]}
283
292
'direction': direction,
284
'specific_fileids': specific_fileids,
293
'specific_files': specific_files,
285
294
'start_revision': start_revision,
286
295
'end_revision': end_revision,
288
'message_search': message_search,
289
297
'levels': levels,
290
298
'generate_tags': generate_tags,
291
299
'delta_type': delta_type,
292
300
'diff_type': diff_type,
293
301
'exclude_common_ancestry': exclude_common_ancestry,
302
'signature': signature,
304
'omit_merges': omit_merges,
294
305
# Add 'private' attributes for features that may be deprecated
295
306
'_match_using_deltas': _match_using_deltas,
354
383
# Tweak the LogRequest based on what the LogFormatter can handle.
355
384
# (There's no point generating stuff if the formatter can't display it.)
357
rqst['levels'] = lf.get_levels()
386
if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
387
# user didn't specify levels, use whatever the LF can handle:
388
rqst['levels'] = lf.get_levels()
358
390
if not getattr(lf, 'supports_tags', False):
359
391
rqst['generate_tags'] = False
360
392
if not getattr(lf, 'supports_delta', False):
361
393
rqst['delta_type'] = None
362
394
if not getattr(lf, 'supports_diff', False):
363
395
rqst['diff_type'] = None
396
if not getattr(lf, 'supports_signatures', False):
397
rqst['signature'] = False
365
399
# Find and print the interesting revisions
366
400
generator = self._generator_factory(self.branch, rqst)
367
for lr in generator.iter_log_revisions():
402
for lr in generator.iter_log_revisions():
404
except errors.GhostRevisionUnusableHere:
405
raise errors.CommandError(
406
gettext('Further revision history missing.'))
371
409
def _generator_factory(self, branch, rqst):
372
410
"""Make the LogGenerator object to use.
374
412
Subclasses may wish to override this.
376
return _DefaultLogGenerator(branch, rqst)
414
return _DefaultLogGenerator(branch, **rqst)
417
def _log_revision_iterator_using_per_file_graph(
418
branch, delta_type, match, levels, path, start_rev_id, end_rev_id,
419
direction, exclude_common_ancestry):
420
# Get the base revisions, filtering by the revision range.
421
# Note that we always generate the merge revisions because
422
# filter_revisions_touching_path() requires them ...
423
view_revisions = _calc_view_revisions(
424
branch, start_rev_id, end_rev_id,
425
direction, generate_merge_revisions=True,
426
exclude_common_ancestry=exclude_common_ancestry)
427
if not isinstance(view_revisions, list):
428
view_revisions = list(view_revisions)
429
view_revisions = _filter_revisions_touching_path(
430
branch, path, view_revisions,
431
include_merges=levels != 1)
432
return make_log_rev_iterator(
433
branch, view_revisions, delta_type, match)
436
def _log_revision_iterator_using_delta_matching(
437
branch, delta_type, match, levels, specific_files, start_rev_id, end_rev_id,
438
direction, exclude_common_ancestry, limit):
439
# Get the base revisions, filtering by the revision range
440
generate_merge_revisions = levels != 1
441
delayed_graph_generation = not specific_files and (
442
limit or start_rev_id or end_rev_id)
443
view_revisions = _calc_view_revisions(
444
branch, start_rev_id, end_rev_id,
446
generate_merge_revisions=generate_merge_revisions,
447
delayed_graph_generation=delayed_graph_generation,
448
exclude_common_ancestry=exclude_common_ancestry)
450
# Apply the other filters
451
return make_log_rev_iterator(branch, view_revisions,
453
files=specific_files,
457
def _format_diff(branch, rev, diff_type, files=None):
460
:param branch: Branch object
461
:param rev: Revision object
462
:param diff_type: Type of diff to generate
463
:param files: List of files to generate diff for (or None for all)
465
repo = branch.repository
466
if len(rev.parent_ids) == 0:
467
ancestor_id = _mod_revision.NULL_REVISION
469
ancestor_id = rev.parent_ids[0]
470
tree_1 = repo.revision_tree(ancestor_id)
471
tree_2 = repo.revision_tree(rev.revision_id)
472
if diff_type == 'partial' and files is not None:
473
specific_files = files
475
specific_files = None
477
path_encoding = get_diff_header_encoding()
478
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
479
new_label='', path_encoding=path_encoding)
379
483
class _StartNotLinearAncestor(Exception):
383
487
class _DefaultLogGenerator(LogGenerator):
384
488
"""The default generator of log revisions."""
386
def __init__(self, branch, rqst):
491
self, branch, levels=None, limit=None, diff_type=None,
492
delta_type=None, show_signature=None, omit_merges=None,
493
generate_tags=None, specific_files=None, match=None,
494
start_revision=None, end_revision=None, direction=None,
495
exclude_common_ancestry=None, _match_using_deltas=None,
387
497
self.branch = branch
389
if rqst.get('generate_tags') and branch.supports_tags():
500
self.diff_type = diff_type
501
self.delta_type = delta_type
502
self.show_signature = signature
503
self.omit_merges = omit_merges
504
self.specific_files = specific_files
506
self.start_revision = start_revision
507
self.end_revision = end_revision
508
self.direction = direction
509
self.exclude_common_ancestry = exclude_common_ancestry
510
self._match_using_deltas = _match_using_deltas
511
if generate_tags and branch.supports_tags():
390
512
self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
392
514
self.rev_tag_dict = {}
397
519
:return: An iterator yielding LogRevision objects.
400
levels = rqst.get('levels')
401
limit = rqst.get('limit')
402
diff_type = rqst.get('diff_type')
404
522
revision_iterator = self._create_log_revision_iterator()
405
523
for revs in revision_iterator:
406
524
for (rev_id, revno, merge_depth), rev, delta in revs:
407
525
# 0 levels means show everything; merge_depth counts from 0
408
if levels != 0 and merge_depth >= levels:
410
if diff_type is None:
526
if (self.levels != 0 and merge_depth is not None and
527
merge_depth >= self.levels):
529
if self.omit_merges and len(rev.parent_ids) > 1:
532
raise errors.GhostRevisionUnusableHere(rev_id)
533
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)
537
self.branch, rev, self.diff_type,
539
if self.show_signature:
540
signature = format_signature_validity(rev_id, self.branch)
544
rev, revno, merge_depth, delta,
545
self.rev_tag_dict.get(rev_id), diff, signature)
418
if log_count >= limit:
548
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
551
def _create_log_revision_iterator(self):
440
552
"""Create a revision iterator for log.
442
554
: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()
557
start_rev_id, end_rev_id = _get_revision_limits(
558
self.branch, self.start_revision, self.end_revision)
559
if self._match_using_deltas:
560
return _log_revision_iterator_using_delta_matching(
562
delta_type=self.delta_type,
565
specific_files=self.specific_files,
566
start_rev_id=start_rev_id, end_rev_id=end_rev_id,
567
direction=self.direction,
568
exclude_common_ancestry=self.exclude_common_ancestry,
451
571
# We're using the per-file-graph algorithm. This scales really
452
572
# well but only makes sense if there is a single file and it's
453
573
# not a directory
454
file_count = len(self.rqst.get('specific_fileids'))
574
file_count = len(self.specific_files)
455
575
if file_count != 1:
456
raise BzrError("illegal LogRequest: must match-using-deltas "
576
raise errors.BzrError(
577
"illegal LogRequest: must match-using-deltas "
457
578
"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'))
579
return _log_revision_iterator_using_per_file_graph(
581
delta_type=self.delta_type,
584
path=self.specific_files[0],
585
start_rev_id=start_rev_id, end_rev_id=end_rev_id,
586
direction=self.direction,
587
exclude_common_ancestry=self.exclude_common_ancestry
497
591
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
599
a list of the same tuples.
507
601
if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
raise errors.BzrCommandError(
509
'--exclude-common-ancestry requires two different revisions')
602
raise errors.CommandError(gettext(
603
'--exclude-common-ancestry requires two different revisions'))
510
604
if direction not in ('reverse', 'forward'):
511
raise ValueError('invalid direction %r' % direction)
512
br_revno, br_rev_id = branch.last_revision_info()
605
raise ValueError(gettext('invalid direction %r') % direction)
606
br_rev_id = branch.last_revision()
607
if br_rev_id == _mod_revision.NULL_REVISION:
516
610
if (end_rev_id and start_rev_id == end_rev_id
517
611
and (not generate_merge_revisions
518
612
or not _has_merges(branch, end_rev_id))):
519
613
# 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)))
614
return _generate_one_revision(branch, end_rev_id, br_rev_id,
616
if not generate_merge_revisions:
618
# If we only want to see linear revisions, we can iterate ...
619
iter_revs = _linear_view_revisions(
620
branch, start_rev_id, end_rev_id,
621
exclude_common_ancestry=exclude_common_ancestry)
622
# If a start limit was given and it's not obviously an
623
# ancestor of the end limit, check it before outputting anything
624
if (direction == 'forward'
625
or (start_rev_id and not _is_obvious_ancestor(
626
branch, start_rev_id, end_rev_id))):
627
iter_revs = list(iter_revs)
628
if direction == 'forward':
629
iter_revs = reversed(iter_revs)
631
except _StartNotLinearAncestor:
632
# Switch to the slower implementation that may be able to find a
633
# non-obvious ancestor out of the left-hand history.
635
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
636
direction, delayed_graph_generation,
637
exclude_common_ancestry)
638
if direction == 'forward':
639
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
540
646
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)
648
revno_str = _compute_revno_str(branch, rev_id)
544
649
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
652
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
653
delayed_graph_generation,
563
654
exclude_common_ancestry=False):
623
715
return len(parents) > 1
718
def _compute_revno_str(branch, rev_id):
719
"""Compute the revno string from a rev_id.
721
:return: The revno string, or None if the revision is not in the supplied
725
revno = branch.revision_id_to_dotted_revno(rev_id)
726
except errors.NoSuchRevision:
727
# The revision must be outside of this branch
730
return '.'.join(str(n) for n in revno)
626
733
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
734
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
628
735
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)
737
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
738
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
739
except errors.NoSuchRevision:
740
# one or both is not in the branch; not obvious
631
742
if len(start_dotted) == 1 and len(end_dotted) == 1:
632
743
# both on mainline
633
744
return start_dotted[0] <= end_dotted[0]
634
745
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
635
start_dotted[0:1] == end_dotted[0:1]):
746
start_dotted[0:1] == end_dotted[0:1]):
636
747
# both on same development line
637
748
return start_dotted[2] <= end_dotted[2]
646
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
757
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
758
exclude_common_ancestry=False):
647
759
"""Calculate a sequence of revisions to view, newest to oldest.
649
761
:param start_rev_id: the lower revision-id
650
762
:param end_rev_id: the upper revision-id
763
:param exclude_common_ancestry: Whether the start_rev_id should be part of
764
the iterated revisions.
651
765
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
766
dotted_revno will be None for ghosts
652
767
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
is not found walking the left-hand history
768
is not found walking the left-hand history
655
br_revno, br_rev_id = branch.last_revision_info()
656
770
repo = branch.repository
771
graph = repo.get_graph()
657
772
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
773
if branch._format.stores_revno() or \
774
config.GlobalStack().get('calculate_revnos'):
776
br_revno, br_rev_id = branch.last_revision_info()
777
except errors.GhostRevisionsHaveNoRevno:
778
br_rev_id = branch.last_revision()
783
br_rev_id = branch.last_revision()
786
graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
787
(_mod_revision.NULL_REVISION,))
790
revision_id = next(graph_iter)
791
except errors.RevisionNotPresent as e:
793
yield e.revision_id, None, None
795
except StopIteration:
798
yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
799
if cur_revno is not None:
802
br_rev_id = branch.last_revision()
663
803
if end_rev_id is None:
664
804
end_rev_id = br_rev_id
665
805
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
806
graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
807
(_mod_revision.NULL_REVISION,))
810
revision_id = next(graph_iter)
811
except StopIteration:
813
except errors.RevisionNotPresent as e:
815
yield e.revision_id, None, None
674
yield revision_id, revno_str, 0
677
raise _StartNotLinearAncestor()
818
revno_str = _compute_revno_str(branch, revision_id)
819
if not found_start and revision_id == start_rev_id:
820
if not exclude_common_ancestry:
821
yield revision_id, revno_str, 0
825
yield revision_id, revno_str, 0
827
raise _StartNotLinearAncestor()
680
830
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
721
871
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
874
def _rebase_merge_depth(view_revisions):
744
875
"""Adjust depths upwards so the top level is 0."""
745
876
# If either the first or last revision have a merge_depth of 0, we're done
746
877
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])
878
min_depth = min([d for r, n, d in view_revisions])
748
879
if min_depth != 0:
749
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
880
view_revisions = [(r, n, d - min_depth)
881
for r, n, d in view_revisions]
750
882
return view_revisions
753
885
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
754
file_ids=None, direction='reverse'):
886
files=None, direction='reverse'):
755
887
"""Create a revision iterator for log.
757
889
:param branch: The branch being logged.
759
891
:param generate_delta: Whether to generate a delta for each revision.
760
892
Permitted values are None, 'full' and 'partial'.
761
893
: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.
894
:param files: If non empty, only revisions matching one or more of
895
the files are to be kept.
764
896
:param direction: the direction in which view_revisions is sorted
765
897
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
768
900
# Convert view_revisions into (view, None, None) groups to fit with
769
901
# the standard interface here.
770
if type(view_revisions) == list:
902
if isinstance(view_revisions, list):
771
903
# A single batch conversion is faster than many incremental ones.
772
904
# As we have all the data, do a batch conversion.
773
905
nones = [None] * len(view_revisions)
774
log_rev_iterator = iter([zip(view_revisions, nones, nones)])
906
log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
777
909
for view in view_revisions:
781
913
# It would be nicer if log adapters were first class objects
782
914
# with custom parameters. This will do for now. IGC 20090127
783
915
if adapter == _make_delta_filter:
784
log_rev_iterator = adapter(branch, generate_delta,
785
search, log_rev_iterator, file_ids, direction)
916
log_rev_iterator = adapter(
917
branch, generate_delta, search, log_rev_iterator, files,
787
log_rev_iterator = adapter(branch, generate_delta,
788
search, log_rev_iterator)
920
log_rev_iterator = adapter(
921
branch, generate_delta, search, log_rev_iterator)
789
922
return log_rev_iterator
792
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
925
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
793
926
"""Create a filtered iterator of log_rev_iterator matching on a regex.
795
928
:param branch: The branch being logged.
796
929
:param generate_delta: Whether to generate a delta for each revision.
797
:param search: A user text search string.
930
:param match: A dictionary with properties as keys and lists of strings
931
as values. To match, a revision may match any of the supplied strings
932
within a single property but must match at least one string for each
798
934
:param log_rev_iterator: An input iterator containing all revisions that
799
935
could be displayed, in lists.
800
936
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
804
940
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):
941
# Use lazy_compile so mapping to InvalidPattern error occurs.
942
searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
943
for k, v in match.items()]
944
return _filter_re(searchRE, log_rev_iterator)
947
def _filter_re(searchRE, log_rev_iterator):
811
948
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))
949
new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
954
def _match_filter(searchRE, rev):
956
'message': (rev.message,),
957
'committer': (rev.committer,),
958
'author': (rev.get_apparent_authors()),
959
'bugs': list(rev.iter_bugs())
961
strings[''] = [item for inner_list in strings.values()
962
for item in inner_list]
963
for k, v in searchRE:
964
if k in strings and not _match_any_filter(strings[k], v):
969
def _match_any_filter(strings, res):
970
return any(r.search(s) for r in res for s in strings)
819
973
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
820
fileids=None, direction='reverse'):
974
files=None, direction='reverse'):
821
975
"""Add revision deltas to a log iterator if needed.
823
977
:param branch: The branch being logged.
826
980
:param search: A user text search string.
827
981
:param log_rev_iterator: An input iterator containing all revisions that
828
982
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.
983
:param files: If non empty, only revisions matching one or more of
984
the files are to be kept.
831
985
:param direction: the direction in which view_revisions is sorted
832
986
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
835
if not generate_delta and not fileids:
989
if not generate_delta and not files:
836
990
return log_rev_iterator
837
991
return _generate_deltas(branch.repository, log_rev_iterator,
838
generate_delta, fileids, direction)
841
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
992
generate_delta, files, direction)
995
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
843
997
"""Create deltas for each batch of revisions in log_rev_iterator.
845
999
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
1000
files, we stop generating deltas once all files reach the
847
1001
appropriate life-cycle point. If we're receiving data newest to
848
1002
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)
1004
check_files = files is not None and len(files) > 0
1006
file_set = set(files)
853
1007
if direction == 'reverse':
856
1010
stop_on = 'remove'
859
1013
for revs in log_rev_iterator:
860
# If we were matching against fileids and we've run out,
1014
# If we were matching against files and we've run out,
861
1015
# there's nothing left to do
862
if check_fileids and not fileid_set:
1016
if check_files and not file_set:
864
1018
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):
1020
if delta_type == 'full' and not check_files:
1021
deltas = repository.get_revision_deltas(revisions)
1022
for rev, delta in zip(revs, deltas):
869
1023
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):
1025
deltas = repository.get_revision_deltas(
1026
revisions, specific_files=file_set)
1027
for rev, delta in zip(revs, deltas):
874
1029
if delta is None or not delta.has_changed():
877
_update_fileids(delta, fileid_set, stop_on)
1032
_update_files(delta, file_set, stop_on)
878
1033
if delta_type is None:
880
1035
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
1049
def _update_files(delta, files, stop_on):
1050
"""Update the set of files to search based on file lifecycle events.
1052
:param files: a set of files to update
1053
:param stop_on: either 'add' or 'remove' - take files out of the
1054
files set once their add or remove entry is detected respectively
901
1056
if stop_on == 'add':
902
1057
for item in delta.added:
903
if item[1] in fileids:
904
fileids.remove(item[1])
1058
if item.path[1] in files:
1059
files.remove(item.path[1])
1060
for item in delta.copied + delta.renamed:
1061
files.remove(item.path[1])
1062
files.add(item.path[0])
905
1063
elif stop_on == 'delete':
906
1064
for item in delta.removed:
907
if item[1] in fileids:
908
fileids.remove(item[1])
1065
if item.path[0] in files:
1066
files.remove(item.path[0])
1067
for item in delta.copied + delta.renamed:
1068
files.remove(item.path[0])
1069
files.add(item.path[1])
911
1072
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
955
1113
def _get_revision_limits(branch, start_revision, end_revision):
956
1114
"""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.
1116
:param branch: The branch containing the revisions.
1118
:param start_revision: The first revision to be logged, as a RevisionInfo.
1120
:param end_revision: The last revision to be logged, as a RevisionInfo
968
1122
:return: (start_rev_id, end_rev_id) tuple.
970
branch_revno, branch_rev_id = branch.last_revision_info()
971
1124
start_rev_id = None
972
if start_revision is None:
1126
if start_revision is not None:
1127
if not isinstance(start_revision, revisionspec.RevisionInfo):
1128
raise TypeError(start_revision)
1129
start_rev_id = start_revision.rev_id
1130
start_revno = start_revision.revno
1131
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
1134
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)
1136
if end_revision is not None:
1137
if not isinstance(end_revision, revisionspec.RevisionInfo):
1138
raise TypeError(start_revision)
1139
end_rev_id = end_revision.rev_id
1140
end_revno = end_revision.revno
995
if branch_revno != 0:
1142
if branch.last_revision() != _mod_revision.NULL_REVISION:
996
1143
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.")
1144
or end_rev_id == _mod_revision.NULL_REVISION):
1145
raise errors.CommandError(
1146
gettext('Logging revision 0 is invalid.'))
1147
if end_revno is not None and start_revno > end_revno:
1148
raise errors.CommandError(
1149
gettext("Start revision must be older than the end revision."))
1002
1150
return (start_rev_id, end_rev_id)
1084
1233
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.
1236
def _filter_revisions_touching_path(branch, path, view_revisions,
1237
include_merges=True):
1238
r"""Return the list of revision ids which touch a given path.
1135
1240
The function filters view_revisions and returns a subset.
1136
This includes the revisions which directly change the file id,
1241
This includes the revisions which directly change the path,
1137
1242
and the revisions which merge these changes. So if the
1138
1243
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
1322
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1255
1323
"""Reverse revisions by depth.
1257
1325
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,
1326
revision of that depth. There may be no topological justification for this
1259
1327
but it looks much nicer.
1261
1329
# Add a fake revision at start so that we can always attach sub revisions
1312
1384
to indicate which LogRevision attributes it supports:
1314
1386
- 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.
1387
Otherwise the delta attribute may not be populated. The 'delta_format'
1388
attribute describes whether the 'short_status' format (1) or the long
1389
one (2) should be used.
1319
1391
- supports_merge_revisions must be True if this log formatter supports
1320
merge revisions. If not, then only mainline revisions will be passed
1392
merge revisions. If not, then only mainline revisions will be passed
1323
1395
- 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.
1396
The default value is zero meaning display all levels.
1397
This value is only relevant if supports_merge_revisions is True.
1327
1399
- supports_tags must be True if this log formatter supports tags.
1328
Otherwise the tags attribute may not be populated.
1400
Otherwise the tags attribute may not be populated.
1330
1402
- supports_diff must be True if this log formatter supports diffs.
1331
Otherwise the diff attribute may not be populated.
1403
Otherwise the diff attribute may not be populated.
1405
- supports_signatures must be True if this log formatter supports GPG
1333
1408
Plugins can register functions to show custom revision properties using
1334
1409
the properties_handler_registry. The registered function
1335
must respect the following interface description:
1410
must respect the following interface description::
1336
1412
def my_show_properties(properties_dict):
1337
1413
# code that returns a dict {'name':'value'} of the properties
1355
1431
let the log formatter decide.
1356
1432
:param show_advice: whether to show advice at the end of the
1434
:param author_list_handler: callable generating a list of
1435
authors to display for a given revision
1359
1437
self.to_file = to_file
1360
1438
# '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
1439
# and should not try to decode/encode it to unicode to avoid bug
1362
1441
if to_exact_file is not None:
1363
1442
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
1444
# XXX: somewhat hacky; this assumes it's a codec writer; it's
1445
# better for code that expects to get diffs to pass in the exact
1368
1447
self.to_exact_file = getattr(to_file, 'stream', to_file)
1369
1448
self.show_ids = show_ids
1370
1449
self.show_timezone = show_timezone
1371
1450
if delta_format is None:
1372
1451
# Ensures backward compatibility
1373
delta_format = 2 # long format
1452
delta_format = 2 # long format
1374
1453
self.delta_format = delta_format
1375
1454
self.levels = levels
1376
1455
self._show_advice = show_advice
1377
1456
self._merge_count = 0
1457
self._author_list_handler = author_list_handler
1379
1459
def get_levels(self):
1380
1460
"""Get the number of levels to display or 0 for all."""
1414
1494
def short_author(self, rev):
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1495
return self.authors(rev, 'first', short=True, sep=', ')
1497
def authors(self, rev, who, short=False, sep=None):
1498
"""Generate list of authors, taking --authors option into account.
1500
The caller has to specify the name of a author list handler,
1501
as provided by the author list registry, using the ``who``
1502
argument. That name only sets a default, though: when the
1503
user selected a different author list generation using the
1504
``--authors`` command line switch, as represented by the
1505
``author_list_handler`` constructor argument, that value takes
1508
:param rev: The revision for which to generate the list of authors.
1509
:param who: Name of the default handler.
1510
:param short: Whether to shorten names to either name or address.
1511
:param sep: What separator to use for automatic concatenation.
1513
if self._author_list_handler is not None:
1514
# The user did specify --authors, which overrides the default
1515
author_list_handler = self._author_list_handler
1517
# The user didn't specify --authors, so we use the caller's default
1518
author_list_handler = author_list_registry.get(who)
1519
names = author_list_handler(rev)
1521
for i in range(len(names)):
1522
name, address = config.parse_username(names[i])
1528
names = sep.join(names)
1420
1531
def merge_marker(self, revision):
1421
1532
"""Get the merge marker to include in the output or '' if none."""
1513
1630
lines = [_LONG_SEP]
1514
1631
if revision.revno is not None:
1515
1632
lines.append('revno: %s%s' % (revision.revno,
1516
self.merge_marker(revision)))
1633
self.merge_marker(revision)))
1517
1634
if revision.tags:
1518
lines.append('tags: %s' % (', '.join(revision.tags)))
1635
lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
1636
if self.show_ids or revision.revno is None:
1637
lines.append('revision-id: %s' %
1638
(revision.rev.revision_id.decode('utf-8'),))
1519
1639
if self.show_ids:
1520
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1521
1640
for parent_id in revision.rev.parent_ids:
1522
lines.append('parent: %s' % (parent_id,))
1641
lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1523
1642
lines.extend(self.custom_properties(revision.rev))
1525
1644
committer = revision.rev.committer
1526
authors = revision.rev.get_apparent_authors()
1645
authors = self.authors(revision.rev, 'all')
1527
1646
if authors != [committer]:
1528
1647
lines.append('author: %s' % (", ".join(authors),))
1529
1648
lines.append('committer: %s' % (committer,))
1598
1720
to_file = self.to_file
1600
1722
if revision.tags:
1601
tags = ' {%s}' % (', '.join(revision.tags))
1723
tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1602
1724
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)
1725
revision.revno or "", self.short_author(
1727
format_date(revision.rev.timestamp,
1728
revision.rev.timezone or 0,
1729
self.show_timezone, date_fmt="%Y-%m-%d",
1731
tags, self.merge_marker(revision)))
1732
self.show_properties(revision.rev, indent + offset)
1733
if self.show_ids or revision.revno is None:
1611
1734
to_file.write(indent + offset + 'revision-id:%s\n'
1612
% (revision.rev.revision_id,))
1735
% (revision.rev.revision_id.decode('utf-8'),))
1613
1736
if not revision.rev.message:
1614
1737
to_file.write(indent + offset + '(no message)\n')
1661
1784
def log_revision(self, revision):
1662
1785
indent = ' ' * revision.merge_depth
1663
1786
self.to_file.write(self.log_string(revision.revno, revision.rev,
1664
self._max_chars, revision.tags, indent))
1787
self._max_chars, revision.tags, indent))
1665
1788
self.to_file.write('\n')
1667
1790
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1791
"""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
1793
:param revno: revision number or None.
1794
Revision numbers counts from 1.
1795
:param rev: revision object
1796
:param max_chars: maximum length of resulting string
1797
:param tags: list of tags or None
1798
:param prefix: string to prefix each line
1799
:return: formatted truncated string
1679
1803
# show revno only when is not None
1680
1804
out.append("%s:" % revno)
1681
out.append(self.truncate(self.short_author(rev), 20))
1805
if max_chars is not None:
1806
out.append(self.truncate(
1807
self.short_author(rev), (max_chars + 3) // 4))
1809
out.append(self.short_author(rev))
1682
1810
out.append(self.date_string(rev))
1683
1811
if len(rev.parent_ids) > 1:
1684
1812
out.append('[merge]')
1686
tag_str = '{%s}' % (', '.join(tags))
1814
tag_str = '{%s}' % (', '.join(sorted(tags)))
1687
1815
out.append(tag_str)
1688
1816
out.append(rev.get_summary())
1689
1817
return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1772
1904
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1773
1905
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)
1906
raise errors.CommandError(
1907
gettext("unknown log formatter: %r") % name)
1910
def author_list_all(rev):
1911
return rev.get_apparent_authors()[:]
1914
def author_list_first(rev):
1915
lst = rev.get_apparent_authors()
1922
def author_list_committer(rev):
1923
return [rev.committer]
1926
author_list_registry = registry.Registry()
1928
author_list_registry.register('all', author_list_all,
1931
author_list_registry.register('first', author_list_first,
1934
author_list_registry.register('committer', author_list_committer,
1783
1938
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1800
1955
# This is the first index which is different between
1802
1957
base_idx = None
1803
for i in xrange(max(len(new_rh),
1958
for i in range(max(len(new_rh), len(old_rh))):
1805
1959
if (len(new_rh) <= i
1806
1960
or len(old_rh) <= i
1807
or new_rh[i] != old_rh[i]):
1961
or new_rh[i] != old_rh[i]):
1811
1965
if base_idx is None:
1812
1966
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
1968
# TODO: It might be nice to do something like show_log
1969
# and show the merged entries. But since this is the
1970
# removed revisions, it shouldn't be as important
1817
1971
if base_idx < len(old_rh):
1818
to_file.write('*'*60)
1972
to_file.write('*' * 60)
1819
1973
to_file.write('\nRemoved Revisions:\n')
1820
1974
for i in range(base_idx, len(old_rh)):
1821
1975
rev = branch.repository.get_revision(old_rh[i])
1822
lr = LogRevision(rev, i+1, 0, None)
1976
lr = LogRevision(rev, i + 1, 0, None)
1823
1977
lf.log_revision(lr)
1824
to_file.write('*'*60)
1978
to_file.write('*' * 60)
1825
1979
to_file.write('\n\n')
1826
1980
if base_idx < len(new_rh):
1827
1981
to_file.write('Added Revisions:\n')
1828
1982
show_log(branch,
1832
1985
direction='forward',
1833
start_revision=base_idx+1,
1834
end_revision=len(new_rh),
1986
start_revision=base_idx + 1,
1987
end_revision=len(new_rh))
1838
1990
def get_history_change(old_revision_id, new_revision_id, repository):
1940
2092
:param file_list: the list of paths given on the command line;
1941
2093
the first of these can be a branch location or a file path,
1942
2094
the remainder must be file paths
2095
:param exit_stack: When the branch returned is read locked,
2096
an unlock call will be queued to the exit stack.
1943
2097
:return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2098
info_list is a list of (relative_path, file_id, kind) tuples where
1945
2099
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2100
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])
2102
from breezy.builtins import _get_revision_range
2103
tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2105
exit_stack.enter_context(b.lock_read())
1951
2106
# XXX: It's damn messy converting a list of paths to relative paths when
1952
2107
# those paths might be deleted ones, they might be on a case-insensitive
1953
2108
# filesystem and/or they might be in silly locations (like another branch).
2018
2173
tree1 = b.repository.revision_tree(rev_id)
2019
2174
file_id = tree1.path2id(fp)
2020
kind = _get_kind_for_file_id(tree1, file_id)
2175
kind = _get_kind_for_file_id(tree1, fp)
2021
2176
info_list.append((fp, file_id, kind))
2022
2177
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)
2180
def _get_kind_for_file_id(tree, path):
2181
"""Return the kind of a path or None if it doesn't exist."""
2182
with tree.lock_read():
2184
return tree.stored_kind(path)
2185
except errors.NoSuchFile:
2033
2189
properties_handler_registry = registry.Registry()
2035
2191
# Use the properties handlers to print out bug information if available
2036
2194
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']
2196
related_bug_urls = []
2197
for bug_url, status in revision.iter_bugs():
2198
if status == 'fixed':
2199
fixed_bug_urls.append(bug_url)
2200
elif status == 'related':
2201
related_bug_urls.append(bug_url)
2204
text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
2205
ret[text] = ' '.join(fixed_bug_urls)
2206
if related_bug_urls:
2207
text = ngettext('related bug', 'related bugs',
2208
len(related_bug_urls))
2209
ret[text] = ' '.join(related_bug_urls)
2044
return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2047
2213
properties_handler_registry.register('bugs_properties_handler',
2048
2214
_bugs_properties_handler)