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
68
revision as _mod_revision,
71
from breezy.i18n import gettext, ngettext
84
from bzrlib.osutils import (
79
from .osutils import (
86
81
format_date_with_offset_in_original_timezone,
82
get_diff_header_encoding,
87
83
get_terminal_encoding,
91
from bzrlib.symbol_versioning import (
97
def find_touching_revisions(branch, file_id):
91
from .tree import find_previous_path
94
def find_touching_revisions(repository, last_revision, last_tree, last_path):
98
95
"""Yield a description of revisions which affect the file_id.
100
97
Each returned element is (revno, revision_id, description)
105
102
TODO: Perhaps some way to limit this to only particular revisions,
106
103
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
105
last_verifier = last_tree.get_file_verifier(last_path)
106
graph = repository.get_graph()
107
history = list(graph.iter_lefthand_ancestry(last_revision, []))
109
for revision_id in history:
110
this_tree = repository.revision_tree(revision_id)
111
this_path = find_previous_path(last_tree, this_tree, 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,
199
if isinstance(start_revision, int):
201
start_revision = revisionspec.RevisionInfo(branch, start_revision)
202
except errors.NoSuchRevision:
203
raise errors.InvalidRevisionNumber(start_revision)
205
if isinstance(end_revision, int):
207
end_revision = revisionspec.RevisionInfo(branch, end_revision)
208
except errors.NoSuchRevision:
209
raise errors.InvalidRevisionNumber(end_revision)
211
if end_revision is not None and end_revision.revno == 0:
212
raise errors.InvalidRevisionNumber(end_revision.revno)
209
214
# Build the request and execute it
210
215
rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
211
start_revision=start_revision, end_revision=end_revision,
212
limit=limit, message_search=search,
213
delta_type=delta_type, diff_type=diff_type)
216
start_revision=start_revision, end_revision=end_revision,
217
limit=limit, message_search=search,
218
delta_type=delta_type, diff_type=diff_type)
214
219
Logger(branch, rqst).show(lf)
217
# Note: This needs to be kept this in sync with the defaults in
222
# Note: This needs to be kept in sync with the defaults in
218
223
# make_log_request_dict() below
219
224
_DEFAULT_REQUEST_PARAMS = {
220
225
'direction': 'reverse',
222
227
'generate_tags': True,
223
228
'exclude_common_ancestry': False,
224
229
'_match_using_deltas': True,
228
233
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
234
start_revision=None, end_revision=None, limit=None,
230
message_search=None, levels=1, generate_tags=True,
235
message_search=None, levels=None, generate_tags=True,
232
237
diff_type=None, _match_using_deltas=True,
233
exclude_common_ancestry=False,
238
exclude_common_ancestry=False, match=None,
239
signature=False, omit_merges=False,
235
241
"""Convenience function for making a logging request dictionary.
274
281
:param _match_using_deltas: a private parameter controlling the
275
282
algorithm used for matching specific_fileids. This parameter
276
may be removed in the future so bzrlib client code should NOT
283
may be removed in the future so breezy client code should NOT
279
286
:param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
287
range operator or as a graph difference.
289
:param signature: show digital signature information
291
:param match: Dictionary of list of search strings to use when filtering
292
revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
293
the empty string to match any of the preceding properties.
295
:param omit_merges: If True, commits with more than one parent are
299
# Take care of old style message_search parameter
302
if 'message' in match:
303
match['message'].append(message_search)
305
match['message'] = [message_search]
307
match = {'message': [message_search]}
283
309
'direction': direction,
284
310
'specific_fileids': specific_fileids,
285
311
'start_revision': start_revision,
286
312
'end_revision': end_revision,
288
'message_search': message_search,
289
314
'levels': levels,
290
315
'generate_tags': generate_tags,
291
316
'delta_type': delta_type,
292
317
'diff_type': diff_type,
293
318
'exclude_common_ancestry': exclude_common_ancestry,
319
'signature': signature,
321
'omit_merges': omit_merges,
294
322
# Add 'private' attributes for features that may be deprecated
295
323
'_match_using_deltas': _match_using_deltas,
299
327
def _apply_log_request_defaults(rqst):
300
328
"""Apply default values to a request dictionary."""
301
result = _DEFAULT_REQUEST_PARAMS
329
result = _DEFAULT_REQUEST_PARAMS.copy()
303
331
result.update(rqst)
335
def format_signature_validity(rev_id, branch):
336
"""get the signature validity
338
:param rev_id: revision id to validate
339
:param branch: branch of revision
340
:return: human readable string to print to log
342
from breezy import gpg
344
gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
345
result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
346
if result[0] == gpg.SIGNATURE_VALID:
347
return u"valid signature from {0}".format(result[1])
348
if result[0] == gpg.SIGNATURE_KEY_MISSING:
349
return "unknown key {0}".format(result[1])
350
if result[0] == gpg.SIGNATURE_NOT_VALID:
351
return "invalid signature!"
352
if result[0] == gpg.SIGNATURE_NOT_SIGNED:
353
return "no signature"
307
356
class LogGenerator(object):
308
357
"""A generator of log revisions."""
354
400
# Tweak the LogRequest based on what the LogFormatter can handle.
355
401
# (There's no point generating stuff if the formatter can't display it.)
357
rqst['levels'] = lf.get_levels()
403
if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
404
# user didn't specify levels, use whatever the LF can handle:
405
rqst['levels'] = lf.get_levels()
358
407
if not getattr(lf, 'supports_tags', False):
359
408
rqst['generate_tags'] = False
360
409
if not getattr(lf, 'supports_delta', False):
361
410
rqst['delta_type'] = None
362
411
if not getattr(lf, 'supports_diff', False):
363
412
rqst['diff_type'] = None
413
if not getattr(lf, 'supports_signatures', False):
414
rqst['signature'] = False
365
416
# Find and print the interesting revisions
366
417
generator = self._generator_factory(self.branch, rqst)
367
for lr in generator.iter_log_revisions():
419
for lr in generator.iter_log_revisions():
421
except errors.GhostRevisionUnusableHere:
422
raise errors.BzrCommandError(
423
gettext('Further revision history missing.'))
371
426
def _generator_factory(self, branch, rqst):
372
427
"""Make the LogGenerator object to use.
374
429
Subclasses may wish to override this.
376
431
return _DefaultLogGenerator(branch, rqst)
400
455
levels = rqst.get('levels')
401
456
limit = rqst.get('limit')
402
457
diff_type = rqst.get('diff_type')
458
show_signature = rqst.get('signature')
459
omit_merges = rqst.get('omit_merges')
404
461
revision_iterator = self._create_log_revision_iterator()
405
462
for revs in revision_iterator:
406
463
for (rev_id, revno, merge_depth), rev, delta in revs:
407
464
# 0 levels means show everything; merge_depth counts from 0
408
if levels != 0 and merge_depth >= levels:
465
if levels != 0 and merge_depth is not None and merge_depth >= levels:
467
if omit_merges and len(rev.parent_ids) > 1:
470
raise errors.GhostRevisionUnusableHere(rev_id)
410
471
if diff_type is None:
413
474
diff = self._format_diff(rev, rev_id, diff_type)
476
signature = format_signature_validity(rev_id, self.branch)
414
479
yield LogRevision(rev, revno, merge_depth, delta,
415
self.rev_tag_dict.get(rev_id), diff)
480
self.rev_tag_dict.get(rev_id), diff, signature)
418
483
if log_count >= limit:
473
539
# Apply the other filters
474
540
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'))
541
rqst.get('delta_type'), rqst.get('match'),
542
file_ids=rqst.get('specific_fileids'),
543
direction=rqst.get('direction'))
479
545
def _log_revision_iterator_using_per_file_graph(self):
480
546
# Get the base revisions, filtering by the revision range.
488
554
if not isinstance(view_revisions, list):
489
555
view_revisions = list(view_revisions)
490
556
view_revisions = _filter_revisions_touching_file_id(self.branch,
491
rqst.get('specific_fileids')[0], view_revisions,
492
include_merges=rqst.get('levels') != 1)
557
rqst.get('specific_fileids')[
559
include_merges=rqst.get('levels') != 1)
493
560
return make_log_rev_iterator(self.branch, view_revisions,
494
rqst.get('delta_type'), rqst.get('message_search'))
561
rqst.get('delta_type'), rqst.get('match'))
497
564
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
572
a list of the same tuples.
507
574
if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
raise errors.BzrCommandError(
509
'--exclude-common-ancestry requires two different revisions')
575
raise errors.BzrCommandError(gettext(
576
'--exclude-common-ancestry requires two different revisions'))
510
577
if direction not in ('reverse', 'forward'):
511
raise ValueError('invalid direction %r' % direction)
512
br_revno, br_rev_id = branch.last_revision_info()
578
raise ValueError(gettext('invalid direction %r') % direction)
579
br_rev_id = branch.last_revision()
580
if br_rev_id == _mod_revision.NULL_REVISION:
516
583
if (end_rev_id and start_rev_id == end_rev_id
517
584
and (not generate_merge_revisions
518
or not _has_merges(branch, end_rev_id))):
585
or not _has_merges(branch, end_rev_id))):
519
586
# 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)))
587
return _generate_one_revision(branch, end_rev_id, br_rev_id,
589
if not generate_merge_revisions:
591
# If we only want to see linear revisions, we can iterate ...
592
iter_revs = _linear_view_revisions(
593
branch, start_rev_id, end_rev_id,
594
exclude_common_ancestry=exclude_common_ancestry)
595
# If a start limit was given and it's not obviously an
596
# ancestor of the end limit, check it before outputting anything
597
if (direction == 'forward'
598
or (start_rev_id and not _is_obvious_ancestor(
599
branch, start_rev_id, end_rev_id))):
600
iter_revs = list(iter_revs)
601
if direction == 'forward':
602
iter_revs = reversed(iter_revs)
604
except _StartNotLinearAncestor:
605
# Switch to the slower implementation that may be able to find a
606
# non-obvious ancestor out of the left-hand history.
608
iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
609
direction, delayed_graph_generation,
610
exclude_common_ancestry)
611
if direction == 'forward':
612
iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
540
619
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)
621
revno_str = _compute_revno_str(branch, rev_id)
544
622
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
625
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
626
delayed_graph_generation,
563
627
exclude_common_ancestry=False):
571
635
initial_revisions = []
572
636
if delayed_graph_generation:
574
for rev_id, revno, depth in _linear_view_revisions(
575
branch, start_rev_id, end_rev_id):
638
for rev_id, revno, depth in _linear_view_revisions(
639
branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
640
if _has_merges(branch, rev_id):
577
641
# The end_rev_id can be nested down somewhere. We need an
578
642
# explicit ancestry check. There is an ambiguity here as we
610
674
# shown naturally, i.e. just like it is for linear logging. We can easily
611
675
# make forward the exact opposite display, but showing the merge revisions
612
676
# indented at the end seems slightly nicer in that case.
613
view_revisions = chain(iter(initial_revisions),
614
_graph_view_revisions(branch, start_rev_id, end_rev_id,
615
rebase_initial_depths=(direction == 'reverse'),
616
exclude_common_ancestry=exclude_common_ancestry))
677
view_revisions = itertools.chain(iter(initial_revisions),
678
_graph_view_revisions(branch, start_rev_id, end_rev_id,
679
rebase_initial_depths=(
680
direction == 'reverse'),
681
exclude_common_ancestry=exclude_common_ancestry))
617
682
return view_revisions
623
688
return len(parents) > 1
691
def _compute_revno_str(branch, rev_id):
692
"""Compute the revno string from a rev_id.
694
:return: The revno string, or None if the revision is not in the supplied
698
revno = branch.revision_id_to_dotted_revno(rev_id)
699
except errors.NoSuchRevision:
700
# The revision must be outside of this branch
703
return '.'.join(str(n) for n in revno)
626
706
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
707
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
628
708
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)
710
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
711
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
712
except errors.NoSuchRevision:
713
# one or both is not in the branch; not obvious
631
715
if len(start_dotted) == 1 and len(end_dotted) == 1:
632
716
# both on mainline
633
717
return start_dotted[0] <= end_dotted[0]
634
718
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
635
start_dotted[0:1] == end_dotted[0:1]):
719
start_dotted[0:1] == end_dotted[0:1]):
636
720
# both on same development line
637
721
return start_dotted[2] <= end_dotted[2]
646
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
730
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
731
exclude_common_ancestry=False):
647
732
"""Calculate a sequence of revisions to view, newest to oldest.
649
734
:param start_rev_id: the lower revision-id
650
735
:param end_rev_id: the upper revision-id
736
:param exclude_common_ancestry: Whether the start_rev_id should be part of
737
the iterated revisions.
651
738
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
739
dotted_revno will be None for ghosts
652
740
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
is not found walking the left-hand history
741
is not found walking the left-hand history
655
br_revno, br_rev_id = branch.last_revision_info()
656
743
repo = branch.repository
744
graph = repo.get_graph()
657
745
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
747
br_revno, br_rev_id = branch.last_revision_info()
748
except errors.GhostRevisionsHaveNoRevno:
749
br_rev_id = branch.last_revision()
753
graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
754
(_mod_revision.NULL_REVISION,))
757
revision_id = next(graph_iter)
758
except errors.RevisionNotPresent as e:
760
yield e.revision_id, None, None
762
except StopIteration:
765
yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
766
if cur_revno is not None:
769
br_rev_id = branch.last_revision()
663
770
if end_rev_id is None:
664
771
end_rev_id = br_rev_id
665
772
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
773
graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
774
(_mod_revision.NULL_REVISION,))
777
revision_id = next(graph_iter)
778
except StopIteration:
780
except errors.RevisionNotPresent as e:
782
yield e.revision_id, None, None
674
yield revision_id, revno_str, 0
677
raise _StartNotLinearAncestor()
785
revno_str = _compute_revno_str(branch, revision_id)
786
if not found_start and revision_id == start_rev_id:
787
if not exclude_common_ancestry:
788
yield revision_id, revno_str, 0
792
yield revision_id, revno_str, 0
794
raise _StartNotLinearAncestor()
680
797
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
721
838
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
841
def _rebase_merge_depth(view_revisions):
744
842
"""Adjust depths upwards so the top level is 0."""
745
843
# If either the first or last revision have a merge_depth of 0, we're done
746
844
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])
845
min_depth = min([d for r, n, d in view_revisions])
748
846
if min_depth != 0:
749
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
847
view_revisions = [(r, n, d - min_depth)
848
for r, n, d in view_revisions]
750
849
return view_revisions
753
852
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
754
file_ids=None, direction='reverse'):
853
file_ids=None, direction='reverse'):
755
854
"""Create a revision iterator for log.
757
856
:param branch: The branch being logged.
768
867
# Convert view_revisions into (view, None, None) groups to fit with
769
868
# the standard interface here.
770
if type(view_revisions) == list:
869
if isinstance(view_revisions, list):
771
870
# A single batch conversion is faster than many incremental ones.
772
871
# As we have all the data, do a batch conversion.
773
872
nones = [None] * len(view_revisions)
774
log_rev_iterator = iter([zip(view_revisions, nones, nones)])
873
log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
777
876
for view in view_revisions:
782
881
# with custom parameters. This will do for now. IGC 20090127
783
882
if adapter == _make_delta_filter:
784
883
log_rev_iterator = adapter(branch, generate_delta,
785
search, log_rev_iterator, file_ids, direction)
884
search, log_rev_iterator, file_ids, direction)
787
886
log_rev_iterator = adapter(branch, generate_delta,
788
search, log_rev_iterator)
887
search, log_rev_iterator)
789
888
return log_rev_iterator
792
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
891
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
793
892
"""Create a filtered iterator of log_rev_iterator matching on a regex.
795
894
:param branch: The branch being logged.
796
895
:param generate_delta: Whether to generate a delta for each revision.
797
:param search: A user text search string.
896
:param match: A dictionary with properties as keys and lists of strings
897
as values. To match, a revision may match any of the supplied strings
898
within a single property but must match at least one string for each
798
900
:param log_rev_iterator: An input iterator containing all revisions that
799
901
could be displayed, in lists.
800
902
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
804
906
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):
907
searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
908
for k, v in match.items()]
909
return _filter_re(searchRE, log_rev_iterator)
912
def _filter_re(searchRE, log_rev_iterator):
811
913
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))
914
new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
919
def _match_filter(searchRE, rev):
921
'message': (rev.message,),
922
'committer': (rev.committer,),
923
'author': (rev.get_apparent_authors()),
924
'bugs': list(rev.iter_bugs())
926
strings[''] = [item for inner_list in strings.values()
927
for item in inner_list]
928
for (k, v) in searchRE:
929
if k in strings and not _match_any_filter(strings[k], v):
934
def _match_any_filter(strings, res):
935
return any(re.search(s) for re in res for s in strings)
819
938
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
820
fileids=None, direction='reverse'):
939
fileids=None, direction='reverse'):
821
940
"""Add revision deltas to a log iterator if needed.
823
942
:param branch: The branch being logged.
835
954
if not generate_delta and not fileids:
836
955
return log_rev_iterator
837
956
return _generate_deltas(branch.repository, log_rev_iterator,
838
generate_delta, fileids, direction)
957
generate_delta, fileids, direction)
841
960
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
843
962
"""Create deltas for each batch of revisions in log_rev_iterator.
845
964
If we're only generating deltas for the sake of filtering against
866
985
if delta_type == 'full' and not check_fileids:
867
986
deltas = repository.get_deltas_for_revisions(revisions)
868
for rev, delta in izip(revs, deltas):
987
for rev, delta in zip(revs, deltas):
869
988
new_revs.append((rev[0], rev[1], delta))
871
990
deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
872
for rev, delta in izip(revs, deltas):
991
for rev, delta in zip(revs, deltas):
873
992
if check_fileids:
874
993
if delta is None or not delta.has_changed():
923
1042
for revs in log_rev_iterator:
924
1043
# r = revision_id, n = revno, d = merge depth
925
1044
revision_ids = [view[0] for view, _, _ in revs]
926
revisions = repository.get_revisions(revision_ids)
927
revs = [(rev[0], revision, rev[2]) for rev, revision in
928
izip(revs, revisions)]
1045
revisions = dict(repository.iter_revisions(revision_ids))
1046
yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
932
1049
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
968
1083
:return: (start_rev_id, end_rev_id) tuple.
970
branch_revno, branch_rev_id = branch.last_revision_info()
971
1085
start_rev_id = None
972
if start_revision is None:
1087
if start_revision is not None:
1088
if not isinstance(start_revision, revisionspec.RevisionInfo):
1089
raise TypeError(start_revision)
1090
start_rev_id = start_revision.rev_id
1091
start_revno = start_revision.revno
1092
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
1095
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)
1097
if end_revision is not None:
1098
if not isinstance(end_revision, revisionspec.RevisionInfo):
1099
raise TypeError(start_revision)
1100
end_rev_id = end_revision.rev_id
1101
end_revno = end_revision.revno
1102
if end_revno is None:
1104
end_revno = branch.revno()
1105
except errors.GhostRevisionsHaveNoRevno:
995
if branch_revno != 0:
1108
if branch.last_revision() != _mod_revision.NULL_REVISION:
996
1109
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.")
1110
or end_rev_id == _mod_revision.NULL_REVISION):
1111
raise errors.BzrCommandError(
1112
gettext('Logging revision 0 is invalid.'))
1113
if end_revno is not None and start_revno > end_revno:
1114
raise errors.BzrCommandError(gettext("Start revision must be "
1115
"older than the end revision."))
1002
1116
return (start_rev_id, end_rev_id)
1052
1166
end_revno = end_revision
1054
1168
if ((start_rev_id == _mod_revision.NULL_REVISION)
1055
or (end_rev_id == _mod_revision.NULL_REVISION)):
1056
raise errors.BzrCommandError('Logging revision 0 is invalid.')
1169
or (end_rev_id == _mod_revision.NULL_REVISION)):
1170
raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1057
1171
if start_revno > end_revno:
1058
raise errors.BzrCommandError("Start revision must be older than "
1059
"the end revision.")
1172
raise errors.BzrCommandError(gettext("Start revision must be older "
1173
"than the end revision."))
1061
1175
if end_revno < start_revno:
1062
1176
return None, None, None, None
1063
1177
cur_revno = branch_revno
1065
1179
mainline_revs = []
1066
for revision_id in branch.repository.iter_reverse_revision_history(
1067
branch_last_revision):
1180
graph = branch.repository.get_graph()
1181
for revision_id in graph.iter_lefthand_ancestry(
1182
branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1183
if cur_revno < start_revno:
1069
1184
# We have gone far enough, but we always add 1 more revision
1070
1185
rev_nos[revision_id] = cur_revno
1084
1199
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
1202
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1132
include_merges=True):
1203
include_merges=True):
1133
1204
r"""Return the list of revision ids which touch a given file id.
1135
1206
The function filters view_revisions and returns a subset.
1136
1207
This includes the revisions which directly change the file id,
1137
1208
and the revisions which merge these changes. So if the
1138
1209
revision graph is::
1177
1251
# indexing layer. We might consider passing in hints as to the known
1178
1252
# access pattern (sparse/clustered, high success rate/low success
1179
1253
# rate). This particular access is clustered with a low success rate.
1180
get_parent_map = branch.repository.texts.get_parent_map
1181
1254
modified_text_revisions = set()
1182
1255
chunk_size = 1000
1183
for start in xrange(0, len(text_keys), chunk_size):
1256
for start in range(0, len(text_keys), chunk_size):
1184
1257
next_keys = text_keys[start:start + chunk_size]
1185
1258
# Only keep the revision_id portion of the key
1186
1259
modified_text_revisions.update(
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
1286
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1255
1287
"""Reverse revisions by depth.
1312
1348
to indicate which LogRevision attributes it supports:
1314
1350
- 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.
1351
Otherwise the delta attribute may not be populated. The 'delta_format'
1352
attribute describes whether the 'short_status' format (1) or the long
1353
one (2) should be used.
1319
1355
- supports_merge_revisions must be True if this log formatter supports
1320
merge revisions. If not, then only mainline revisions will be passed
1356
merge revisions. If not, then only mainline revisions will be passed
1323
1359
- 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.
1360
The default value is zero meaning display all levels.
1361
This value is only relevant if supports_merge_revisions is True.
1327
1363
- supports_tags must be True if this log formatter supports tags.
1328
Otherwise the tags attribute may not be populated.
1364
Otherwise the tags attribute may not be populated.
1330
1366
- supports_diff must be True if this log formatter supports diffs.
1331
Otherwise the diff attribute may not be populated.
1367
Otherwise the diff attribute may not be populated.
1369
- supports_signatures must be True if this log formatter supports GPG
1333
1372
Plugins can register functions to show custom revision properties using
1334
1373
the properties_handler_registry. The registered function
1335
must respect the following interface description:
1374
must respect the following interface description::
1336
1376
def my_show_properties(properties_dict):
1337
1377
# code that returns a dict {'name':'value'} of the properties
1342
1382
def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1383
delta_format=None, levels=None, show_advice=False,
1344
to_exact_file=None):
1384
to_exact_file=None, author_list_handler=None):
1345
1385
"""Create a LogFormatter.
1347
1387
:param to_file: the file to output to
1348
:param to_exact_file: if set, gives an output stream to which
1388
:param to_exact_file: if set, gives an output stream to which
1349
1389
non-Unicode diffs are written.
1350
1390
:param show_ids: if True, revision-ids are to be displayed
1351
1391
:param show_timezone: the timezone to use
1414
1457
def short_author(self, rev):
1415
name, address = config.parse_username(rev.get_apparent_authors()[0])
1458
return self.authors(rev, 'first', short=True, sep=', ')
1460
def authors(self, rev, who, short=False, sep=None):
1461
"""Generate list of authors, taking --authors option into account.
1463
The caller has to specify the name of a author list handler,
1464
as provided by the author list registry, using the ``who``
1465
argument. That name only sets a default, though: when the
1466
user selected a different author list generation using the
1467
``--authors`` command line switch, as represented by the
1468
``author_list_handler`` constructor argument, that value takes
1471
:param rev: The revision for which to generate the list of authors.
1472
:param who: Name of the default handler.
1473
:param short: Whether to shorten names to either name or address.
1474
:param sep: What separator to use for automatic concatenation.
1476
if self._author_list_handler is not None:
1477
# The user did specify --authors, which overrides the default
1478
author_list_handler = self._author_list_handler
1480
# The user didn't specify --authors, so we use the caller's default
1481
author_list_handler = author_list_registry.get(who)
1482
names = author_list_handler(rev)
1484
for i in range(len(names)):
1485
name, address = config.parse_username(names[i])
1491
names = sep.join(names)
1420
1494
def merge_marker(self, revision):
1421
1495
"""Get the merge marker to include in the output or '' if none."""
1513
1589
lines = [_LONG_SEP]
1514
1590
if revision.revno is not None:
1515
1591
lines.append('revno: %s%s' % (revision.revno,
1516
self.merge_marker(revision)))
1592
self.merge_marker(revision)))
1517
1593
if revision.tags:
1518
lines.append('tags: %s' % (', '.join(revision.tags)))
1594
lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
1595
if self.show_ids or revision.revno is None:
1596
lines.append('revision-id: %s' %
1597
(revision.rev.revision_id.decode('utf-8'),))
1519
1598
if self.show_ids:
1520
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1521
1599
for parent_id in revision.rev.parent_ids:
1522
lines.append('parent: %s' % (parent_id,))
1600
lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1523
1601
lines.extend(self.custom_properties(revision.rev))
1525
1603
committer = revision.rev.committer
1526
authors = revision.rev.get_apparent_authors()
1604
authors = self.authors(revision.rev, 'all')
1527
1605
if authors != [committer]:
1528
1606
lines.append('author: %s' % (", ".join(authors),))
1529
1607
lines.append('committer: %s' % (committer,))
1547
1628
to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1548
1629
if revision.delta is not None:
1549
1630
# Use the standard status output to display changes
1550
from bzrlib.delta import report_delta
1551
report_delta(to_file, revision.delta, short_status=False,
1631
from breezy.delta import report_delta
1632
report_delta(to_file, revision.delta, short_status=False,
1552
1633
show_ids=self.show_ids, indent=indent)
1553
1634
if revision.diff is not None:
1554
1635
to_file.write(indent + 'diff:\n')
1598
1679
to_file = self.to_file
1600
1681
if revision.tags:
1601
tags = ' {%s}' % (', '.join(revision.tags))
1682
tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1602
1683
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)
1684
revision.revno or "", self.short_author(
1686
format_date(revision.rev.timestamp,
1687
revision.rev.timezone or 0,
1688
self.show_timezone, date_fmt="%Y-%m-%d",
1690
tags, self.merge_marker(revision)))
1691
self.show_properties(revision.rev, indent + offset)
1692
if self.show_ids or revision.revno is None:
1611
1693
to_file.write(indent + offset + 'revision-id:%s\n'
1612
% (revision.rev.revision_id,))
1694
% (revision.rev.revision_id.decode('utf-8'),))
1613
1695
if not revision.rev.message:
1614
1696
to_file.write(indent + offset + '(no message)\n')
1620
1702
if revision.delta is not None:
1621
1703
# Use the standard status output to display changes
1622
from bzrlib.delta import report_delta
1623
report_delta(to_file, revision.delta,
1624
short_status=self.delta_format==1,
1704
from breezy.delta import report_delta
1705
report_delta(to_file, revision.delta,
1706
short_status=self.delta_format == 1,
1625
1707
show_ids=self.show_ids, indent=indent + offset)
1626
1708
if revision.diff is not None:
1627
1709
self.show_diff(self.to_exact_file, revision.diff, ' ')
1661
1743
def log_revision(self, revision):
1662
1744
indent = ' ' * revision.merge_depth
1663
1745
self.to_file.write(self.log_string(revision.revno, revision.rev,
1664
self._max_chars, revision.tags, indent))
1746
self._max_chars, revision.tags, indent))
1665
1747
self.to_file.write('\n')
1667
1749
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1750
"""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
1752
:param revno: revision number or None.
1753
Revision numbers counts from 1.
1754
:param rev: revision object
1755
:param max_chars: maximum length of resulting string
1756
:param tags: list of tags or None
1757
:param prefix: string to prefix each line
1758
:return: formatted truncated string
1679
1762
# show revno only when is not None
1680
1763
out.append("%s:" % revno)
1681
out.append(self.truncate(self.short_author(rev), 20))
1764
if max_chars is not None:
1765
out.append(self.truncate(
1766
self.short_author(rev), (max_chars + 3) // 4))
1768
out.append(self.short_author(rev))
1682
1769
out.append(self.date_string(rev))
1683
1770
if len(rev.parent_ids) > 1:
1684
1771
out.append('[merge]')
1686
tag_str = '{%s}' % (', '.join(tags))
1773
tag_str = '{%s}' % (', '.join(sorted(tags)))
1687
1774
out.append(tag_str)
1688
1775
out.append(rev.get_summary())
1689
1776
return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1703
1790
self.show_timezone,
1704
1791
date_fmt='%Y-%m-%d',
1705
1792
show_offset=False)
1706
committer_str = revision.rev.get_apparent_authors()[0].replace (' <', ' <')
1707
to_file.write('%s %s\n\n' % (date_str,committer_str))
1793
committer_str = self.authors(revision.rev, 'first', sep=', ')
1794
committer_str = committer_str.replace(' <', ' <')
1795
to_file.write('%s %s\n\n' % (date_str, committer_str))
1709
1797
if revision.delta is not None and revision.delta.has_changed():
1710
1798
for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1712
1800
to_file.write('\t* %s:\n' % (path,))
1713
1801
for c in revision.delta.renamed:
1714
oldpath,newpath = c[:2]
1802
oldpath, newpath = c[:2]
1715
1803
# For renamed files, show both the old and the new path
1716
to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
1804
to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
1717
1805
to_file.write('\n')
1719
1807
if not revision.rev.message:
1742
1830
return self.get(name)(*args, **kwargs)
1744
1832
def get_default(self, branch):
1745
return self.get(branch.get_config().log_format())
1833
c = branch.get_config_stack()
1834
return self.get(c.get('log_format'))
1748
1837
log_formatter_registry = LogFormatterRegistry()
1751
1840
log_formatter_registry.register('short', ShortLogFormatter,
1752
'Moderately short log format')
1841
'Moderately short log format.')
1753
1842
log_formatter_registry.register('long', LongLogFormatter,
1754
'Detailed log format')
1843
'Detailed log format.')
1755
1844
log_formatter_registry.register('line', LineLogFormatter,
1756
'Log format with one line per revision')
1845
'Log format with one line per revision.')
1757
1846
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1758
'Format used by GNU ChangeLog files')
1847
'Format used by GNU ChangeLog files.')
1761
1850
def register_formatter(name, formatter):
1772
1861
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1773
1862
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)
1863
raise errors.BzrCommandError(
1864
gettext("unknown log formatter: %r") % name)
1867
def author_list_all(rev):
1868
return rev.get_apparent_authors()[:]
1871
def author_list_first(rev):
1872
lst = rev.get_apparent_authors()
1879
def author_list_committer(rev):
1880
return [rev.committer]
1883
author_list_registry = registry.Registry()
1885
author_list_registry.register('all', author_list_all,
1888
author_list_registry.register('first', author_list_first,
1891
author_list_registry.register('committer', author_list_committer,
1783
1895
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1800
1912
# This is the first index which is different between
1802
1914
base_idx = None
1803
for i in xrange(max(len(new_rh),
1915
for i in range(max(len(new_rh), len(old_rh))):
1805
1916
if (len(new_rh) <= i
1806
1917
or len(old_rh) <= i
1807
or new_rh[i] != old_rh[i]):
1918
or new_rh[i] != old_rh[i]):
1811
1922
if base_idx is None:
1812
1923
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
1925
# TODO: It might be nice to do something like show_log
1926
# and show the merged entries. But since this is the
1927
# removed revisions, it shouldn't be as important
1817
1928
if base_idx < len(old_rh):
1818
to_file.write('*'*60)
1929
to_file.write('*' * 60)
1819
1930
to_file.write('\nRemoved Revisions:\n')
1820
1931
for i in range(base_idx, len(old_rh)):
1821
1932
rev = branch.repository.get_revision(old_rh[i])
1822
lr = LogRevision(rev, i+1, 0, None)
1933
lr = LogRevision(rev, i + 1, 0, None)
1823
1934
lf.log_revision(lr)
1824
to_file.write('*'*60)
1935
to_file.write('*' * 60)
1825
1936
to_file.write('\n\n')
1826
1937
if base_idx < len(new_rh):
1827
1938
to_file.write('Added Revisions:\n')
1903
2015
log_format = log_formatter_registry.get_default(branch)
1904
2016
lf = log_format(show_ids=False, to_file=output, show_timezone='original')
1905
2017
if old_history != []:
1906
output.write('*'*60)
2018
output.write('*' * 60)
1907
2019
output.write('\nRemoved Revisions:\n')
1908
2020
show_flat_log(branch.repository, old_history, old_revno, lf)
1909
output.write('*'*60)
2021
output.write('*' * 60)
1910
2022
output.write('\n\n')
1911
2023
if new_history != []:
1912
2024
output.write('Added Revisions:\n')
1913
2025
start_revno = new_revno - len(new_history) + 1
1914
2026
show_log(branch, lf, None, verbose=False, direction='forward',
1915
start_revision=start_revno,)
2027
start_revision=start_revno)
1918
2030
def show_flat_log(repository, history, last_revno, lf):
1940
2052
:param file_list: the list of paths given on the command line;
1941
2053
the first of these can be a branch location or a file path,
1942
2054
the remainder must be file paths
2055
:param add_cleanup: When the branch returned is read locked,
2056
an unlock call will be queued to the cleanup.
1943
2057
:return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2058
info_list is a list of (relative_path, file_id, kind) tuples where
1945
2059
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2060
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])
2062
from breezy.builtins import _get_revision_range
2063
tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2065
add_cleanup(b.lock_read().unlock)
1951
2066
# XXX: It's damn messy converting a list of paths to relative paths when
1952
2067
# those paths might be deleted ones, they might be on a case-insensitive
1953
2068
# filesystem and/or they might be in silly locations (like another branch).
1957
2072
# case of running log in a nested directory, assuming paths beyond the
1958
2073
# first one haven't been deleted ...
1960
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2075
relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1962
2077
relpaths = [path] + file_list[1:]
1964
2079
start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
1966
2081
if relpaths in ([], [u'']):
1967
2082
return b, [], start_rev_info, end_rev_info
1968
2083
if start_rev_info is None and end_rev_info is None:
2018
2133
tree1 = b.repository.revision_tree(rev_id)
2019
2134
file_id = tree1.path2id(fp)
2020
kind = _get_kind_for_file_id(tree1, file_id)
2135
kind = _get_kind_for_file_id(tree1, fp, file_id)
2021
2136
info_list.append((fp, file_id, kind))
2022
2137
return b, info_list, start_rev_info, end_rev_info
2025
def _get_kind_for_file_id(tree, file_id):
2140
def _get_kind_for_file_id(tree, path, file_id):
2026
2141
"""Return the kind of a file-id or None if it doesn't exist."""
2027
2142
if file_id is not None:
2028
return tree.kind(file_id)
2143
return tree.kind(path, file_id)
2033
2148
properties_handler_registry = registry.Registry()
2035
2150
# Use the properties handlers to print out bug information if available
2036
2153
def _bugs_properties_handler(revision):
2037
if revision.properties.has_key('bugs'):
2154
if 'bugs' in revision.properties:
2038
2155
bug_lines = revision.properties['bugs'].split('\n')
2039
2156
bug_rows = [line.split(' ', 1) for line in bug_lines]
2040
2157
fixed_bug_urls = [row[0] for row in bug_rows if
2041
2158
len(row) > 1 and row[1] == 'fixed']
2043
2160
if fixed_bug_urls:
2044
return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2161
return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):
2162
' '.join(fixed_bug_urls)}
2047
2166
properties_handler_registry.register('bugs_properties_handler',
2048
2167
_bugs_properties_handler)