1
# Copyright (C) 2005, 2006, 2007, 2009 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
"""Code to show logs of changes.
21
Various flavors of log can be produced:
23
* for one file, or the whole tree, and (not done yet) for
24
files in a given directory
26
* in "verbose" mode with a description of what changed from one
29
* with file-ids and revision-ids shown
31
Logs are actually written out through an abstract LogFormatter
32
interface, which allows for different preferred formats. Plugins can
35
Logs can be produced in either forward (oldest->newest) or reverse
36
(newest->oldest) order.
38
Logs can be filtered to show only revisions matching a particular
39
search string, or within a particular range of revisions. The range
40
can be given as date/times, which are reduced to revisions before
43
In verbose mode we show a summary of what changed in each particular
44
revision. Note that this is the delta for changes in that revision
45
relative to its left-most parent, not the delta relative to the last
46
logged revision. So for example if you ask for a verbose log of
47
changes touching hello.c you will get a list of those revisions also
48
listing other things that were changed in the same revision, but not
49
all the changes since the previous revision that touched hello.c.
53
from cStringIO import StringIO
54
from itertools import (
60
from warnings import (
64
from bzrlib.lazy_import import lazy_import
65
lazy_import(globals(), """
73
repository as _mod_repository,
74
revision as _mod_revision,
84
from bzrlib.osutils import (
86
format_date_with_offset_in_original_timezone,
87
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.
100
Each returned element is (revno, revision_id, description)
102
This is the list of revisions where the file is either added,
103
modified, renamed or deleted.
105
TODO: Perhaps some way to limit this to only particular revisions,
106
or to traverse a non-mainline set of revisions?
111
for revision_id in branch.revision_history():
112
this_inv = branch.repository.get_revision_inventory(revision_id)
113
if file_id in this_inv:
114
this_ie = this_inv[file_id]
115
this_path = this_inv.id2path(file_id)
117
this_ie = this_path = None
119
# now we know how it was last time, and how it is in this revision.
120
# 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
130
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
137
last_path = this_path
141
def _enumerate_history(branch):
144
for rev_id in branch.revision_history():
145
rh.append((revno, rev_id))
152
specific_fileid=None,
160
"""Write out human-readable log of commits to this branch.
162
This function is being retained for backwards compatibility but
163
should not be extended with new parameters. Use the new Logger class
164
instead, eg. Logger(branch, rqst).show(lf), adding parameters to the
165
make_log_request_dict function.
167
:param lf: The LogFormatter object showing the output.
169
:param specific_fileid: If not None, list only the commits affecting the
170
specified file, rather than all commits.
172
:param verbose: If True show added/changed/deleted/renamed files.
174
:param direction: 'reverse' (default) is latest to earliest; 'forward' is
177
:param start_revision: If not None, only show revisions >= start_revision
179
:param end_revision: If not None, only show revisions <= end_revision
181
:param search: If not None, only show revisions with matching commit
184
:param limit: If set, shows only 'limit' revisions, all revisions are shown
187
:param show_diff: If True, output a diff after each 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'
203
diff_type = 'partial'
209
# Build the request and execute it
210
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)
214
Logger(branch, rqst).show(lf)
217
# Note: This needs to be kept this in sync with the defaults in
218
# make_log_request_dict() below
219
_DEFAULT_REQUEST_PARAMS = {
220
'direction': 'reverse',
222
'generate_tags': True,
223
'_match_using_deltas': True,
227
def make_log_request_dict(direction='reverse', specific_fileids=None,
228
start_revision=None, end_revision=None, limit=None,
229
message_search=None, levels=1, generate_tags=True, delta_type=None,
230
diff_type=None, _match_using_deltas=True):
231
"""Convenience function for making a logging request dictionary.
233
Using this function may make code slightly safer by ensuring
234
parameters have the correct names. It also provides a reference
235
point for documenting the supported parameters.
237
:param direction: 'reverse' (default) is latest to earliest;
238
'forward' is earliest to latest.
240
:param specific_fileids: If not None, only include revisions
241
affecting the specified files, rather than all revisions.
243
:param start_revision: If not None, only generate
244
revisions >= start_revision
246
:param end_revision: If not None, only generate
247
revisions <= end_revision
249
:param limit: If set, generate only 'limit' revisions, all revisions
250
are shown if None or 0.
252
:param message_search: If not None, only include revisions with
253
matching commit messages
255
:param levels: the number of levels of revisions to
256
generate; 1 for just the mainline; 0 for all levels.
258
:param generate_tags: If True, include tags for matched revisions.
260
:param delta_type: Either 'full', 'partial' or None.
261
'full' means generate the complete delta - adds/deletes/modifies/etc;
262
'partial' means filter the delta using specific_fileids;
263
None means do not generate any delta.
265
:param diff_type: Either 'full', 'partial' or None.
266
'full' means generate the complete diff - adds/deletes/modifies/etc;
267
'partial' means filter the diff using specific_fileids;
268
None means do not generate any diff.
270
:param _match_using_deltas: a private parameter controlling the
271
algorithm used for matching specific_fileids. This parameter
272
may be removed in the future so bzrlib client code should NOT
276
'direction': direction,
277
'specific_fileids': specific_fileids,
278
'start_revision': start_revision,
279
'end_revision': end_revision,
281
'message_search': message_search,
283
'generate_tags': generate_tags,
284
'delta_type': delta_type,
285
'diff_type': diff_type,
286
# Add 'private' attributes for features that may be deprecated
287
'_match_using_deltas': _match_using_deltas,
291
def _apply_log_request_defaults(rqst):
292
"""Apply default values to a request dictionary."""
293
result = _DEFAULT_REQUEST_PARAMS
299
class LogGenerator(object):
300
"""A generator of log revisions."""
302
def iter_log_revisions(self):
303
"""Iterate over LogRevision objects.
305
:return: An iterator yielding LogRevision objects.
307
raise NotImplementedError(self.iter_log_revisions)
310
class Logger(object):
311
"""An object that generates, formats and displays a log."""
313
def __init__(self, branch, rqst):
316
:param branch: the branch to log
317
:param rqst: A dictionary specifying the query parameters.
318
See make_log_request_dict() for supported values.
321
self.rqst = _apply_log_request_defaults(rqst)
326
:param lf: The LogFormatter object to send the output to.
328
if not isinstance(lf, LogFormatter):
329
warn("not a LogFormatter instance: %r" % lf)
331
self.branch.lock_read()
333
if getattr(lf, 'begin_log', None):
336
if getattr(lf, 'end_log', None):
341
def _show_body(self, lf):
342
"""Show the main log output.
344
Subclasses may wish to override this.
346
# Tweak the LogRequest based on what the LogFormatter can handle.
347
# (There's no point generating stuff if the formatter can't display it.)
349
rqst['levels'] = lf.get_levels()
350
if not getattr(lf, 'supports_tags', False):
351
rqst['generate_tags'] = False
352
if not getattr(lf, 'supports_delta', False):
353
rqst['delta_type'] = None
354
if not getattr(lf, 'supports_diff', False):
355
rqst['diff_type'] = None
357
# Find and print the interesting revisions
358
generator = self._generator_factory(self.branch, rqst)
359
for lr in generator.iter_log_revisions():
363
def _generator_factory(self, branch, rqst):
364
"""Make the LogGenerator object to use.
366
Subclasses may wish to override this.
368
return _DefaultLogGenerator(branch, rqst)
371
class _StartNotLinearAncestor(Exception):
372
"""Raised when a start revision is not found walking left-hand history."""
375
class _DefaultLogGenerator(LogGenerator):
376
"""The default generator of log revisions."""
378
def __init__(self, branch, rqst):
381
if rqst.get('generate_tags') and branch.supports_tags():
382
self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
384
self.rev_tag_dict = {}
386
def iter_log_revisions(self):
387
"""Iterate over LogRevision objects.
389
:return: An iterator yielding LogRevision objects.
392
levels = rqst.get('levels')
393
limit = rqst.get('limit')
394
diff_type = rqst.get('diff_type')
396
revision_iterator = self._create_log_revision_iterator()
397
for revs in revision_iterator:
398
for (rev_id, revno, merge_depth), rev, delta in revs:
399
# 0 levels means show everything; merge_depth counts from 0
400
if levels != 0 and merge_depth >= levels:
402
if diff_type is None:
405
diff = self._format_diff(rev, rev_id, diff_type)
406
yield LogRevision(rev, revno, merge_depth, delta,
407
self.rev_tag_dict.get(rev_id), diff)
410
if log_count >= limit:
413
def _format_diff(self, rev, rev_id, diff_type):
414
repo = self.branch.repository
415
if len(rev.parent_ids) == 0:
416
ancestor_id = _mod_revision.NULL_REVISION
418
ancestor_id = rev.parent_ids[0]
419
tree_1 = repo.revision_tree(ancestor_id)
420
tree_2 = repo.revision_tree(rev_id)
421
file_ids = self.rqst.get('specific_fileids')
422
if diff_type == 'partial' and file_ids is not None:
423
specific_files = [tree_2.id2path(id) for id in file_ids]
425
specific_files = None
427
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
431
def _create_log_revision_iterator(self):
432
"""Create a revision iterator for log.
434
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
437
self.start_rev_id, self.end_rev_id = _get_revision_limits(
438
self.branch, self.rqst.get('start_revision'),
439
self.rqst.get('end_revision'))
440
if self.rqst.get('_match_using_deltas'):
441
return self._log_revision_iterator_using_delta_matching()
443
# We're using the per-file-graph algorithm. This scales really
444
# well but only makes sense if there is a single file and it's
446
file_count = len(self.rqst.get('specific_fileids'))
448
raise BzrError("illegal LogRequest: must match-using-deltas "
449
"when logging %d files" % file_count)
450
return self._log_revision_iterator_using_per_file_graph()
452
def _log_revision_iterator_using_delta_matching(self):
453
# Get the base revisions, filtering by the revision range
455
generate_merge_revisions = rqst.get('levels') != 1
456
delayed_graph_generation = not rqst.get('specific_fileids') and (
457
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
458
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
459
self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
460
delayed_graph_generation=delayed_graph_generation)
462
# Apply the other filters
463
return make_log_rev_iterator(self.branch, view_revisions,
464
rqst.get('delta_type'), rqst.get('message_search'),
465
file_ids=rqst.get('specific_fileids'),
466
direction=rqst.get('direction'))
468
def _log_revision_iterator_using_per_file_graph(self):
469
# Get the base revisions, filtering by the revision range.
470
# Note that we always generate the merge revisions because
471
# filter_revisions_touching_file_id() requires them ...
473
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
474
self.end_rev_id, rqst.get('direction'), True)
475
if not isinstance(view_revisions, list):
476
view_revisions = list(view_revisions)
477
view_revisions = _filter_revisions_touching_file_id(self.branch,
478
rqst.get('specific_fileids')[0], view_revisions,
479
include_merges=rqst.get('levels') != 1)
480
return make_log_rev_iterator(self.branch, view_revisions,
481
rqst.get('delta_type'), rqst.get('message_search'))
484
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
485
generate_merge_revisions, delayed_graph_generation=False):
486
"""Calculate the revisions to view.
488
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
489
a list of the same tuples.
491
br_revno, br_rev_id = branch.last_revision_info()
495
# If a single revision is requested, check we can handle it
496
generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
497
(not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
498
if generate_single_revision:
499
return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
501
# If we only want to see linear revisions, we can iterate ...
502
if not generate_merge_revisions:
503
return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
506
return _generate_all_revisions(branch, start_rev_id, end_rev_id,
507
direction, delayed_graph_generation)
510
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
511
if rev_id == br_rev_id:
513
return [(br_rev_id, br_revno, 0)]
515
revno = branch.revision_id_to_dotted_revno(rev_id)
516
revno_str = '.'.join(str(n) for n in revno)
517
return [(rev_id, revno_str, 0)]
520
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
521
result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
522
# If a start limit was given and it's not obviously an
523
# ancestor of the end limit, check it before outputting anything
524
if direction == 'forward' or (start_rev_id
525
and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
527
result = list(result)
528
except _StartNotLinearAncestor:
529
raise errors.BzrCommandError('Start revision not found in'
530
' left-hand history of end revision.')
531
if direction == 'forward':
532
result = reversed(result)
536
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
537
delayed_graph_generation):
538
# On large trees, generating the merge graph can take 30-60 seconds
539
# so we delay doing it until a merge is detected, incrementally
540
# returning initial (non-merge) revisions while we can.
541
initial_revisions = []
542
if delayed_graph_generation:
544
for rev_id, revno, depth in \
545
_linear_view_revisions(branch, start_rev_id, end_rev_id):
546
if _has_merges(branch, rev_id):
550
initial_revisions.append((rev_id, revno, depth))
552
# No merged revisions found
553
if direction == 'reverse':
554
return initial_revisions
555
elif direction == 'forward':
556
return reversed(initial_revisions)
558
raise ValueError('invalid direction %r' % direction)
559
except _StartNotLinearAncestor:
560
# A merge was never detected so the lower revision limit can't
561
# be nested down somewhere
562
raise errors.BzrCommandError('Start revision not found in'
563
' history of end revision.')
565
# A log including nested merges is required. If the direction is reverse,
566
# we rebase the initial merge depths so that the development line is
567
# shown naturally, i.e. just like it is for linear logging. We can easily
568
# make forward the exact opposite display, but showing the merge revisions
569
# indented at the end seems slightly nicer in that case.
570
view_revisions = chain(iter(initial_revisions),
571
_graph_view_revisions(branch, start_rev_id, end_rev_id,
572
rebase_initial_depths=direction == 'reverse'))
573
if direction == 'reverse':
574
return view_revisions
575
elif direction == 'forward':
576
# Forward means oldest first, adjusting for depth.
577
view_revisions = reverse_by_depth(list(view_revisions))
578
return _rebase_merge_depth(view_revisions)
580
raise ValueError('invalid direction %r' % direction)
583
def _has_merges(branch, rev_id):
584
"""Does a revision have multiple parents or not?"""
585
parents = branch.repository.get_parent_map([rev_id]).get(rev_id, [])
586
return len(parents) > 1
589
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
590
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
591
if start_rev_id and end_rev_id:
592
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
593
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
594
if len(start_dotted) == 1 and len(end_dotted) == 1:
596
return start_dotted[0] <= end_dotted[0]
597
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
598
start_dotted[0:1] == end_dotted[0:1]):
599
# both on same development line
600
return start_dotted[2] <= end_dotted[2]
604
# if either start or end is not specified then we use either the first or
605
# the last revision and *they* are obvious ancestors.
609
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
610
"""Calculate a sequence of revisions to view, newest to oldest.
612
:param start_rev_id: the lower revision-id
613
:param end_rev_id: the upper revision-id
614
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
615
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
616
is not found walking the left-hand history
618
br_revno, br_rev_id = branch.last_revision_info()
619
repo = branch.repository
620
if start_rev_id is None and end_rev_id is None:
622
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
623
yield revision_id, str(cur_revno), 0
626
if end_rev_id is None:
627
end_rev_id = br_rev_id
628
found_start = start_rev_id is None
629
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
630
revno = branch.revision_id_to_dotted_revno(revision_id)
631
revno_str = '.'.join(str(n) for n in revno)
632
if not found_start and revision_id == start_rev_id:
633
yield revision_id, revno_str, 0
637
yield revision_id, revno_str, 0
640
raise _StartNotLinearAncestor()
643
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
644
rebase_initial_depths=True):
645
"""Calculate revisions to view including merges, newest to oldest.
647
:param branch: the branch
648
:param start_rev_id: the lower revision-id
649
:param end_rev_id: the upper revision-id
650
:param rebase_initial_depth: should depths be rebased until a mainline
652
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
654
view_revisions = branch.iter_merge_sorted_revisions(
655
start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
656
stop_rule="with-merges")
657
if not rebase_initial_depths:
658
for (rev_id, merge_depth, revno, end_of_merge
660
yield rev_id, '.'.join(map(str, revno)), merge_depth
662
# We're following a development line starting at a merged revision.
663
# We need to adjust depths down by the initial depth until we find
664
# a depth less than it. Then we use that depth as the adjustment.
665
# If and when we reach the mainline, depth adjustment ends.
666
depth_adjustment = None
667
for (rev_id, merge_depth, revno, end_of_merge
669
if depth_adjustment is None:
670
depth_adjustment = merge_depth
672
if merge_depth < depth_adjustment:
673
# From now on we reduce the depth adjustement, this can be
674
# surprising for users. The alternative requires two passes
675
# which breaks the fast display of the first revision
677
depth_adjustment = merge_depth
678
merge_depth -= depth_adjustment
679
yield rev_id, '.'.join(map(str, revno)), merge_depth
682
@deprecated_function(deprecated_in((2, 2, 0)))
683
def calculate_view_revisions(branch, start_revision, end_revision, direction,
684
specific_fileid, generate_merge_revisions):
685
"""Calculate the revisions to view.
687
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
688
a list of the same tuples.
690
start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
692
view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
693
direction, generate_merge_revisions or specific_fileid))
695
view_revisions = _filter_revisions_touching_file_id(branch,
696
specific_fileid, view_revisions,
697
include_merges=generate_merge_revisions)
698
return _rebase_merge_depth(view_revisions)
701
def _rebase_merge_depth(view_revisions):
702
"""Adjust depths upwards so the top level is 0."""
703
# If either the first or last revision have a merge_depth of 0, we're done
704
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
705
min_depth = min([d for r,n,d in view_revisions])
707
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
708
return view_revisions
711
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
712
file_ids=None, direction='reverse'):
713
"""Create a revision iterator for log.
715
:param branch: The branch being logged.
716
:param view_revisions: The revisions being viewed.
717
:param generate_delta: Whether to generate a delta for each revision.
718
Permitted values are None, 'full' and 'partial'.
719
:param search: A user text search string.
720
:param file_ids: If non empty, only revisions matching one or more of
721
the file-ids are to be kept.
722
:param direction: the direction in which view_revisions is sorted
723
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
726
# Convert view_revisions into (view, None, None) groups to fit with
727
# the standard interface here.
728
if type(view_revisions) == list:
729
# A single batch conversion is faster than many incremental ones.
730
# As we have all the data, do a batch conversion.
731
nones = [None] * len(view_revisions)
732
log_rev_iterator = iter([zip(view_revisions, nones, nones)])
735
for view in view_revisions:
736
yield (view, None, None)
737
log_rev_iterator = iter([_convert()])
738
for adapter in log_adapters:
739
# It would be nicer if log adapters were first class objects
740
# with custom parameters. This will do for now. IGC 20090127
741
if adapter == _make_delta_filter:
742
log_rev_iterator = adapter(branch, generate_delta,
743
search, log_rev_iterator, file_ids, direction)
745
log_rev_iterator = adapter(branch, generate_delta,
746
search, log_rev_iterator)
747
return log_rev_iterator
750
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
751
"""Create a filtered iterator of log_rev_iterator matching on a regex.
753
:param branch: The branch being logged.
754
:param generate_delta: Whether to generate a delta for each revision.
755
:param search: A user text search string.
756
:param log_rev_iterator: An input iterator containing all revisions that
757
could be displayed, in lists.
758
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
762
return log_rev_iterator
763
searchRE = re_compile_checked(search, re.IGNORECASE,
764
'log message filter')
765
return _filter_message_re(searchRE, log_rev_iterator)
768
def _filter_message_re(searchRE, log_rev_iterator):
769
for revs in log_rev_iterator:
771
for (rev_id, revno, merge_depth), rev, delta in revs:
772
if searchRE.search(rev.message):
773
new_revs.append(((rev_id, revno, merge_depth), rev, delta))
777
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
778
fileids=None, direction='reverse'):
779
"""Add revision deltas to a log iterator if needed.
781
:param branch: The branch being logged.
782
:param generate_delta: Whether to generate a delta for each revision.
783
Permitted values are None, 'full' and 'partial'.
784
:param search: A user text search string.
785
:param log_rev_iterator: An input iterator containing all revisions that
786
could be displayed, in lists.
787
:param fileids: If non empty, only revisions matching one or more of
788
the file-ids are to be kept.
789
:param direction: the direction in which view_revisions is sorted
790
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
793
if not generate_delta and not fileids:
794
return log_rev_iterator
795
return _generate_deltas(branch.repository, log_rev_iterator,
796
generate_delta, fileids, direction)
799
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
801
"""Create deltas for each batch of revisions in log_rev_iterator.
803
If we're only generating deltas for the sake of filtering against
804
file-ids, we stop generating deltas once all file-ids reach the
805
appropriate life-cycle point. If we're receiving data newest to
806
oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
808
check_fileids = fileids is not None and len(fileids) > 0
810
fileid_set = set(fileids)
811
if direction == 'reverse':
817
for revs in log_rev_iterator:
818
# If we were matching against fileids and we've run out,
819
# there's nothing left to do
820
if check_fileids and not fileid_set:
822
revisions = [rev[1] for rev in revs]
824
if delta_type == 'full' and not check_fileids:
825
deltas = repository.get_deltas_for_revisions(revisions)
826
for rev, delta in izip(revs, deltas):
827
new_revs.append((rev[0], rev[1], delta))
829
deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
830
for rev, delta in izip(revs, deltas):
832
if delta is None or not delta.has_changed():
835
_update_fileids(delta, fileid_set, stop_on)
836
if delta_type is None:
838
elif delta_type == 'full':
839
# If the file matches all the time, rebuilding
840
# a full delta like this in addition to a partial
841
# one could be slow. However, it's likely that
842
# most revisions won't get this far, making it
843
# faster to filter on the partial deltas and
844
# build the occasional full delta than always
845
# building full deltas and filtering those.
847
delta = repository.get_revision_delta(rev_id)
848
new_revs.append((rev[0], rev[1], delta))
852
def _update_fileids(delta, fileids, stop_on):
853
"""Update the set of file-ids to search based on file lifecycle events.
855
:param fileids: a set of fileids to update
856
:param stop_on: either 'add' or 'remove' - take file-ids out of the
857
fileids set once their add or remove entry is detected respectively
860
for item in delta.added:
861
if item[1] in fileids:
862
fileids.remove(item[1])
863
elif stop_on == 'delete':
864
for item in delta.removed:
865
if item[1] in fileids:
866
fileids.remove(item[1])
869
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
870
"""Extract revision objects from the repository
872
:param branch: The branch being logged.
873
:param generate_delta: Whether to generate a delta for each revision.
874
:param search: A user text search string.
875
:param log_rev_iterator: An input iterator containing all revisions that
876
could be displayed, in lists.
877
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
880
repository = branch.repository
881
for revs in log_rev_iterator:
882
# r = revision_id, n = revno, d = merge depth
883
revision_ids = [view[0] for view, _, _ in revs]
884
revisions = repository.get_revisions(revision_ids)
885
revs = [(rev[0], revision, rev[2]) for rev, revision in
886
izip(revs, revisions)]
890
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
891
"""Group up a single large batch into smaller ones.
893
:param branch: The branch being logged.
894
:param generate_delta: Whether to generate a delta for each revision.
895
:param search: A user text search string.
896
:param log_rev_iterator: An input iterator containing all revisions that
897
could be displayed, in lists.
898
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
901
repository = branch.repository
903
for batch in log_rev_iterator:
906
step = [detail for _, detail in zip(range(num), batch)]
910
num = min(int(num * 1.5), 200)
913
def _get_revision_limits(branch, start_revision, end_revision):
914
"""Get and check revision limits.
916
:param branch: The branch containing the revisions.
918
:param start_revision: The first revision to be logged.
919
For backwards compatibility this may be a mainline integer revno,
920
but for merge revision support a RevisionInfo is expected.
922
:param end_revision: The last revision to be logged.
923
For backwards compatibility this may be a mainline integer revno,
924
but for merge revision support a RevisionInfo is expected.
926
:return: (start_rev_id, end_rev_id) tuple.
928
branch_revno, branch_rev_id = branch.last_revision_info()
930
if start_revision is None:
933
if isinstance(start_revision, revisionspec.RevisionInfo):
934
start_rev_id = start_revision.rev_id
935
start_revno = start_revision.revno or 1
937
branch.check_real_revno(start_revision)
938
start_revno = start_revision
939
start_rev_id = branch.get_rev_id(start_revno)
942
if end_revision is None:
943
end_revno = branch_revno
945
if isinstance(end_revision, revisionspec.RevisionInfo):
946
end_rev_id = end_revision.rev_id
947
end_revno = end_revision.revno or branch_revno
949
branch.check_real_revno(end_revision)
950
end_revno = end_revision
951
end_rev_id = branch.get_rev_id(end_revno)
953
if branch_revno != 0:
954
if (start_rev_id == _mod_revision.NULL_REVISION
955
or end_rev_id == _mod_revision.NULL_REVISION):
956
raise errors.BzrCommandError('Logging revision 0 is invalid.')
957
if start_revno > end_revno:
958
raise errors.BzrCommandError("Start revision must be older than "
960
return (start_rev_id, end_rev_id)
963
def _get_mainline_revs(branch, start_revision, end_revision):
964
"""Get the mainline revisions from the branch.
966
Generates the list of mainline revisions for the branch.
968
:param branch: The branch containing the revisions.
970
:param start_revision: The first revision to be logged.
971
For backwards compatibility this may be a mainline integer revno,
972
but for merge revision support a RevisionInfo is expected.
974
:param end_revision: The last revision to be logged.
975
For backwards compatibility this may be a mainline integer revno,
976
but for merge revision support a RevisionInfo is expected.
978
:return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
980
branch_revno, branch_last_revision = branch.last_revision_info()
981
if branch_revno == 0:
982
return None, None, None, None
984
# For mainline generation, map start_revision and end_revision to
985
# mainline revnos. If the revision is not on the mainline choose the
986
# appropriate extreme of the mainline instead - the extra will be
988
# Also map the revisions to rev_ids, to be used in the later filtering
991
if start_revision is None:
994
if isinstance(start_revision, revisionspec.RevisionInfo):
995
start_rev_id = start_revision.rev_id
996
start_revno = start_revision.revno or 1
998
branch.check_real_revno(start_revision)
999
start_revno = start_revision
1002
if end_revision is None:
1003
end_revno = branch_revno
1005
if isinstance(end_revision, revisionspec.RevisionInfo):
1006
end_rev_id = end_revision.rev_id
1007
end_revno = end_revision.revno or branch_revno
1009
branch.check_real_revno(end_revision)
1010
end_revno = end_revision
1012
if ((start_rev_id == _mod_revision.NULL_REVISION)
1013
or (end_rev_id == _mod_revision.NULL_REVISION)):
1014
raise errors.BzrCommandError('Logging revision 0 is invalid.')
1015
if start_revno > end_revno:
1016
raise errors.BzrCommandError("Start revision must be older than "
1017
"the end revision.")
1019
if end_revno < start_revno:
1020
return None, None, None, None
1021
cur_revno = branch_revno
1024
for revision_id in branch.repository.iter_reverse_revision_history(
1025
branch_last_revision):
1026
if cur_revno < start_revno:
1027
# We have gone far enough, but we always add 1 more revision
1028
rev_nos[revision_id] = cur_revno
1029
mainline_revs.append(revision_id)
1031
if cur_revno <= end_revno:
1032
rev_nos[revision_id] = cur_revno
1033
mainline_revs.append(revision_id)
1036
# We walked off the edge of all revisions, so we add a 'None' marker
1037
mainline_revs.append(None)
1039
mainline_revs.reverse()
1041
# override the mainline to look like the revision history.
1042
return mainline_revs, rev_nos, start_rev_id, end_rev_id
1045
@deprecated_function(deprecated_in((2, 2, 0)))
1046
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1047
"""Filter view_revisions based on revision ranges.
1049
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1050
tuples to be filtered.
1052
:param start_rev_id: If not NONE specifies the first revision to be logged.
1053
If NONE then all revisions up to the end_rev_id are logged.
1055
:param end_rev_id: If not NONE specifies the last revision to be logged.
1056
If NONE then all revisions up to the end of the log are logged.
1058
:return: The filtered view_revisions.
1060
if start_rev_id or end_rev_id:
1061
revision_ids = [r for r, n, d in view_revisions]
1063
start_index = revision_ids.index(start_rev_id)
1066
if start_rev_id == end_rev_id:
1067
end_index = start_index
1070
end_index = revision_ids.index(end_rev_id)
1072
end_index = len(view_revisions) - 1
1073
# To include the revisions merged into the last revision,
1074
# extend end_rev_id down to, but not including, the next rev
1075
# with the same or lesser merge_depth
1076
end_merge_depth = view_revisions[end_index][2]
1078
for index in xrange(end_index+1, len(view_revisions)+1):
1079
if view_revisions[index][2] <= end_merge_depth:
1080
end_index = index - 1
1083
# if the search falls off the end then log to the end as well
1084
end_index = len(view_revisions) - 1
1085
view_revisions = view_revisions[start_index:end_index+1]
1086
return view_revisions
1089
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1090
include_merges=True):
1091
r"""Return the list of revision ids which touch a given file id.
1093
The function filters view_revisions and returns a subset.
1094
This includes the revisions which directly change the file id,
1095
and the revisions which merge these changes. So if the
1107
And 'C' changes a file, then both C and D will be returned. F will not be
1108
returned even though it brings the changes to C into the branch starting
1109
with E. (Note that if we were using F as the tip instead of G, then we
1112
This will also be restricted based on a subset of the mainline.
1114
:param branch: The branch where we can get text revision information.
1116
:param file_id: Filter out revisions that do not touch file_id.
1118
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1119
tuples. This is the list of revisions which will be filtered. It is
1120
assumed that view_revisions is in merge_sort order (i.e. newest
1123
:param include_merges: include merge revisions in the result or not
1125
:return: A list of (revision_id, dotted_revno, merge_depth) tuples.
1127
# Lookup all possible text keys to determine which ones actually modified
1129
text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1131
# Looking up keys in batches of 1000 can cut the time in half, as well as
1132
# memory consumption. GraphIndex *does* like to look for a few keys in
1133
# parallel, it just doesn't like looking for *lots* of keys in parallel.
1134
# TODO: This code needs to be re-evaluated periodically as we tune the
1135
# indexing layer. We might consider passing in hints as to the known
1136
# access pattern (sparse/clustered, high success rate/low success
1137
# rate). This particular access is clustered with a low success rate.
1138
get_parent_map = branch.repository.texts.get_parent_map
1139
modified_text_revisions = set()
1141
for start in xrange(0, len(text_keys), chunk_size):
1142
next_keys = text_keys[start:start + chunk_size]
1143
# Only keep the revision_id portion of the key
1144
modified_text_revisions.update(
1145
[k[1] for k in get_parent_map(next_keys)])
1146
del text_keys, next_keys
1149
# Track what revisions will merge the current revision, replace entries
1150
# with 'None' when they have been added to result
1151
current_merge_stack = [None]
1152
for info in view_revisions:
1153
rev_id, revno, depth = info
1154
if depth == len(current_merge_stack):
1155
current_merge_stack.append(info)
1157
del current_merge_stack[depth + 1:]
1158
current_merge_stack[-1] = info
1160
if rev_id in modified_text_revisions:
1161
# This needs to be logged, along with the extra revisions
1162
for idx in xrange(len(current_merge_stack)):
1163
node = current_merge_stack[idx]
1164
if node is not None:
1165
if include_merges or node[2] == 0:
1167
current_merge_stack[idx] = None
1171
@deprecated_function(deprecated_in((2, 2, 0)))
1172
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1173
include_merges=True):
1174
"""Produce an iterator of revisions to show
1175
:return: an iterator of (revision_id, revno, merge_depth)
1176
(if there is no revno for a revision, None is supplied)
1178
if not include_merges:
1179
revision_ids = mainline_revs[1:]
1180
if direction == 'reverse':
1181
revision_ids.reverse()
1182
for revision_id in revision_ids:
1183
yield revision_id, str(rev_nos[revision_id]), 0
1185
graph = branch.repository.get_graph()
1186
# This asks for all mainline revisions, which means we only have to spider
1187
# sideways, rather than depth history. That said, its still size-of-history
1188
# and should be addressed.
1189
# mainline_revisions always includes an extra revision at the beginning, so
1191
parent_map = dict(((key, value) for key, value in
1192
graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1193
# filter out ghosts; merge_sort errors on ghosts.
1194
rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1195
merge_sorted_revisions = tsort.merge_sort(
1199
generate_revno=True)
1201
if direction == 'forward':
1202
# forward means oldest first.
1203
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1204
elif direction != 'reverse':
1205
raise ValueError('invalid direction %r' % direction)
1207
for (sequence, rev_id, merge_depth, revno, end_of_merge
1208
) in merge_sorted_revisions:
1209
yield rev_id, '.'.join(map(str, revno)), merge_depth
1212
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1213
"""Reverse revisions by depth.
1215
Revisions with a different depth are sorted as a group with the previous
1216
revision of that depth. There may be no topological justification for this,
1217
but it looks much nicer.
1219
# Add a fake revision at start so that we can always attach sub revisions
1220
merge_sorted_revisions = [(None, None, _depth)] + merge_sorted_revisions
1222
for val in merge_sorted_revisions:
1223
if val[2] == _depth:
1224
# Each revision at the current depth becomes a chunk grouping all
1225
# higher depth revisions.
1226
zd_revisions.append([val])
1228
zd_revisions[-1].append(val)
1229
for revisions in zd_revisions:
1230
if len(revisions) > 1:
1231
# We have higher depth revisions, let reverse them locally
1232
revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
1233
zd_revisions.reverse()
1235
for chunk in zd_revisions:
1236
result.extend(chunk)
1238
# Top level call, get rid of the fake revisions that have been added
1239
result = [r for r in result if r[0] is not None and r[1] is not None]
1243
class LogRevision(object):
1244
"""A revision to be logged (by LogFormatter.log_revision).
1246
A simple wrapper for the attributes of a revision to be logged.
1247
The attributes may or may not be populated, as determined by the
1248
logging options and the log formatter capabilities.
1251
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1252
tags=None, diff=None):
1254
self.revno = str(revno)
1255
self.merge_depth = merge_depth
1261
class LogFormatter(object):
1262
"""Abstract class to display log messages.
1264
At a minimum, a derived class must implement the log_revision method.
1266
If the LogFormatter needs to be informed of the beginning or end of
1267
a log it should implement the begin_log and/or end_log hook methods.
1269
A LogFormatter should define the following supports_XXX flags
1270
to indicate which LogRevision attributes it supports:
1272
- supports_delta must be True if this log formatter supports delta.
1273
Otherwise the delta attribute may not be populated. The 'delta_format'
1274
attribute describes whether the 'short_status' format (1) or the long
1275
one (2) should be used.
1277
- supports_merge_revisions must be True if this log formatter supports
1278
merge revisions. If not, then only mainline revisions will be passed
1281
- preferred_levels is the number of levels this formatter defaults to.
1282
The default value is zero meaning display all levels.
1283
This value is only relevant if supports_merge_revisions is True.
1285
- supports_tags must be True if this log formatter supports tags.
1286
Otherwise the tags attribute may not be populated.
1288
- supports_diff must be True if this log formatter supports diffs.
1289
Otherwise the diff attribute may not be populated.
1291
Plugins can register functions to show custom revision properties using
1292
the properties_handler_registry. The registered function
1293
must respect the following interface description:
1294
def my_show_properties(properties_dict):
1295
# code that returns a dict {'name':'value'} of the properties
1298
preferred_levels = 0
1300
def __init__(self, to_file, show_ids=False, show_timezone='original',
1301
delta_format=None, levels=None, show_advice=False,
1302
to_exact_file=None):
1303
"""Create a LogFormatter.
1305
:param to_file: the file to output to
1306
:param to_exact_file: if set, gives an output stream to which
1307
non-Unicode diffs are written.
1308
:param show_ids: if True, revision-ids are to be displayed
1309
:param show_timezone: the timezone to use
1310
:param delta_format: the level of delta information to display
1311
or None to leave it to the formatter to decide
1312
:param levels: the number of levels to display; None or -1 to
1313
let the log formatter decide.
1314
:param show_advice: whether to show advice at the end of the
1317
self.to_file = to_file
1318
# 'exact' stream used to show diff, it should print content 'as is'
1319
# and should not try to decode/encode it to unicode to avoid bug #328007
1320
if to_exact_file is not None:
1321
self.to_exact_file = to_exact_file
1323
# XXX: somewhat hacky; this assumes it's a codec writer; it's better
1324
# for code that expects to get diffs to pass in the exact file
1326
self.to_exact_file = getattr(to_file, 'stream', to_file)
1327
self.show_ids = show_ids
1328
self.show_timezone = show_timezone
1329
if delta_format is None:
1330
# Ensures backward compatibility
1331
delta_format = 2 # long format
1332
self.delta_format = delta_format
1333
self.levels = levels
1334
self._show_advice = show_advice
1335
self._merge_count = 0
1337
def get_levels(self):
1338
"""Get the number of levels to display or 0 for all."""
1339
if getattr(self, 'supports_merge_revisions', False):
1340
if self.levels is None or self.levels == -1:
1341
self.levels = self.preferred_levels
1346
def log_revision(self, revision):
1349
:param revision: The LogRevision to be logged.
1351
raise NotImplementedError('not implemented in abstract base')
1353
def show_advice(self):
1354
"""Output user advice, if any, when the log is completed."""
1355
if self._show_advice and self.levels == 1 and self._merge_count > 0:
1356
advice_sep = self.get_advice_separator()
1358
self.to_file.write(advice_sep)
1360
"Use --include-merges or -n0 to see merged revisions.\n")
1362
def get_advice_separator(self):
1363
"""Get the text separating the log from the closing advice."""
1366
def short_committer(self, rev):
1367
name, address = config.parse_username(rev.committer)
1372
def short_author(self, rev):
1373
name, address = config.parse_username(rev.get_apparent_authors()[0])
1378
def merge_marker(self, revision):
1379
"""Get the merge marker to include in the output or '' if none."""
1380
if len(revision.rev.parent_ids) > 1:
1381
self._merge_count += 1
1386
def show_properties(self, revision, indent):
1387
"""Displays the custom properties returned by each registered handler.
1389
If a registered handler raises an error it is propagated.
1391
for line in self.custom_properties(revision):
1392
self.to_file.write("%s%s\n" % (indent, line))
1394
def custom_properties(self, revision):
1395
"""Format the custom properties returned by each registered handler.
1397
If a registered handler raises an error it is propagated.
1399
:return: a list of formatted lines (excluding trailing newlines)
1401
lines = self._foreign_info_properties(revision)
1402
for key, handler in properties_handler_registry.iteritems():
1403
lines.extend(self._format_properties(handler(revision)))
1406
def _foreign_info_properties(self, rev):
1407
"""Custom log displayer for foreign revision identifiers.
1409
:param rev: Revision object.
1411
# Revision comes directly from a foreign repository
1412
if isinstance(rev, foreign.ForeignRevision):
1413
return rev.mapping.vcs.show_foreign_revid(rev.foreign_revid)
1415
# Imported foreign revision revision ids always contain :
1416
if not ":" in rev.revision_id:
1419
# Revision was once imported from a foreign repository
1421
foreign_revid, mapping = \
1422
foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1423
except errors.InvalidRevisionId:
1426
return self._format_properties(
1427
mapping.vcs.show_foreign_revid(foreign_revid))
1429
def _format_properties(self, properties):
1431
for key, value in properties.items():
1432
lines.append(key + ': ' + value)
1435
def show_diff(self, to_file, diff, indent):
1436
for l in diff.rstrip().split('\n'):
1437
to_file.write(indent + '%s\n' % (l,))
1440
# Separator between revisions in long format
1441
_LONG_SEP = '-' * 60
1444
class LongLogFormatter(LogFormatter):
1446
supports_merge_revisions = True
1447
preferred_levels = 1
1448
supports_delta = True
1449
supports_tags = True
1450
supports_diff = True
1452
def __init__(self, *args, **kwargs):
1453
super(LongLogFormatter, self).__init__(*args, **kwargs)
1454
if self.show_timezone == 'original':
1455
self.date_string = self._date_string_original_timezone
1457
self.date_string = self._date_string_with_timezone
1459
def _date_string_with_timezone(self, rev):
1460
return format_date(rev.timestamp, rev.timezone or 0,
1463
def _date_string_original_timezone(self, rev):
1464
return format_date_with_offset_in_original_timezone(rev.timestamp,
1467
def log_revision(self, revision):
1468
"""Log a revision, either merged or not."""
1469
indent = ' ' * revision.merge_depth
1471
if revision.revno is not None:
1472
lines.append('revno: %s%s' % (revision.revno,
1473
self.merge_marker(revision)))
1475
lines.append('tags: %s' % (', '.join(revision.tags)))
1477
lines.append('revision-id: %s' % (revision.rev.revision_id,))
1478
for parent_id in revision.rev.parent_ids:
1479
lines.append('parent: %s' % (parent_id,))
1480
lines.extend(self.custom_properties(revision.rev))
1482
committer = revision.rev.committer
1483
authors = revision.rev.get_apparent_authors()
1484
if authors != [committer]:
1485
lines.append('author: %s' % (", ".join(authors),))
1486
lines.append('committer: %s' % (committer,))
1488
branch_nick = revision.rev.properties.get('branch-nick', None)
1489
if branch_nick is not None:
1490
lines.append('branch nick: %s' % (branch_nick,))
1492
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1494
lines.append('message:')
1495
if not revision.rev.message:
1496
lines.append(' (no message)')
1498
message = revision.rev.message.rstrip('\r\n')
1499
for l in message.split('\n'):
1500
lines.append(' %s' % (l,))
1502
# Dump the output, appending the delta and diff if requested
1503
to_file = self.to_file
1504
to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1505
if revision.delta is not None:
1506
# We don't respect delta_format for compatibility
1507
revision.delta.show(to_file, self.show_ids, indent=indent,
1509
if revision.diff is not None:
1510
to_file.write(indent + 'diff:\n')
1512
# Note: we explicitly don't indent the diff (relative to the
1513
# revision information) so that the output can be fed to patch -p0
1514
self.show_diff(self.to_exact_file, revision.diff, indent)
1515
self.to_exact_file.flush()
1517
def get_advice_separator(self):
1518
"""Get the text separating the log from the closing advice."""
1519
return '-' * 60 + '\n'
1522
class ShortLogFormatter(LogFormatter):
1524
supports_merge_revisions = True
1525
preferred_levels = 1
1526
supports_delta = True
1527
supports_tags = True
1528
supports_diff = True
1530
def __init__(self, *args, **kwargs):
1531
super(ShortLogFormatter, self).__init__(*args, **kwargs)
1532
self.revno_width_by_depth = {}
1534
def log_revision(self, revision):
1535
# We need two indents: one per depth and one for the information
1536
# relative to that indent. Most mainline revnos are 5 chars or
1537
# less while dotted revnos are typically 11 chars or less. Once
1538
# calculated, we need to remember the offset for a given depth
1539
# as we might be starting from a dotted revno in the first column
1540
# and we want subsequent mainline revisions to line up.
1541
depth = revision.merge_depth
1542
indent = ' ' * depth
1543
revno_width = self.revno_width_by_depth.get(depth)
1544
if revno_width is None:
1545
if revision.revno.find('.') == -1:
1546
# mainline revno, e.g. 12345
1549
# dotted revno, e.g. 12345.10.55
1551
self.revno_width_by_depth[depth] = revno_width
1552
offset = ' ' * (revno_width + 1)
1554
to_file = self.to_file
1557
tags = ' {%s}' % (', '.join(revision.tags))
1558
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1559
revision.revno, self.short_author(revision.rev),
1560
format_date(revision.rev.timestamp,
1561
revision.rev.timezone or 0,
1562
self.show_timezone, date_fmt="%Y-%m-%d",
1564
tags, self.merge_marker(revision)))
1565
self.show_properties(revision.rev, indent+offset)
1567
to_file.write(indent + offset + 'revision-id:%s\n'
1568
% (revision.rev.revision_id,))
1569
if not revision.rev.message:
1570
to_file.write(indent + offset + '(no message)\n')
1572
message = revision.rev.message.rstrip('\r\n')
1573
for l in message.split('\n'):
1574
to_file.write(indent + offset + '%s\n' % (l,))
1576
if revision.delta is not None:
1577
revision.delta.show(to_file, self.show_ids, indent=indent + offset,
1578
short_status=self.delta_format==1)
1579
if revision.diff is not None:
1580
self.show_diff(self.to_exact_file, revision.diff, ' ')
1584
class LineLogFormatter(LogFormatter):
1586
supports_merge_revisions = True
1587
preferred_levels = 1
1588
supports_tags = True
1590
def __init__(self, *args, **kwargs):
1591
super(LineLogFormatter, self).__init__(*args, **kwargs)
1592
width = terminal_width()
1593
if width is not None:
1594
# we need one extra space for terminals that wrap on last char
1596
self._max_chars = width
1598
def truncate(self, str, max_len):
1599
if max_len is None or len(str) <= max_len:
1601
return str[:max_len-3] + '...'
1603
def date_string(self, rev):
1604
return format_date(rev.timestamp, rev.timezone or 0,
1605
self.show_timezone, date_fmt="%Y-%m-%d",
1608
def message(self, rev):
1610
return '(no message)'
1614
def log_revision(self, revision):
1615
indent = ' ' * revision.merge_depth
1616
self.to_file.write(self.log_string(revision.revno, revision.rev,
1617
self._max_chars, revision.tags, indent))
1618
self.to_file.write('\n')
1620
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1621
"""Format log info into one string. Truncate tail of string
1622
:param revno: revision number or None.
1623
Revision numbers counts from 1.
1624
:param rev: revision object
1625
:param max_chars: maximum length of resulting string
1626
:param tags: list of tags or None
1627
:param prefix: string to prefix each line
1628
:return: formatted truncated string
1632
# show revno only when is not None
1633
out.append("%s:" % revno)
1634
out.append(self.truncate(self.short_author(rev), 20))
1635
out.append(self.date_string(rev))
1636
if len(rev.parent_ids) > 1:
1637
out.append('[merge]')
1639
tag_str = '{%s}' % (', '.join(tags))
1641
out.append(rev.get_summary())
1642
return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1645
class GnuChangelogLogFormatter(LogFormatter):
1647
supports_merge_revisions = True
1648
supports_delta = True
1650
def log_revision(self, revision):
1651
"""Log a revision, either merged or not."""
1652
to_file = self.to_file
1654
date_str = format_date(revision.rev.timestamp,
1655
revision.rev.timezone or 0,
1657
date_fmt='%Y-%m-%d',
1659
committer_str = revision.rev.committer.replace (' <', ' <')
1660
to_file.write('%s %s\n\n' % (date_str,committer_str))
1662
if revision.delta is not None and revision.delta.has_changed():
1663
for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1665
to_file.write('\t* %s:\n' % (path,))
1666
for c in revision.delta.renamed:
1667
oldpath,newpath = c[:2]
1668
# For renamed files, show both the old and the new path
1669
to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
1672
if not revision.rev.message:
1673
to_file.write('\tNo commit message\n')
1675
message = revision.rev.message.rstrip('\r\n')
1676
for l in message.split('\n'):
1677
to_file.write('\t%s\n' % (l.lstrip(),))
1681
def line_log(rev, max_chars):
1682
lf = LineLogFormatter(None)
1683
return lf.log_string(None, rev, max_chars)
1686
class LogFormatterRegistry(registry.Registry):
1687
"""Registry for log formatters"""
1689
def make_formatter(self, name, *args, **kwargs):
1690
"""Construct a formatter from arguments.
1692
:param name: Name of the formatter to construct. 'short', 'long' and
1693
'line' are built-in.
1695
return self.get(name)(*args, **kwargs)
1697
def get_default(self, branch):
1698
return self.get(branch.get_config().log_format())
1701
log_formatter_registry = LogFormatterRegistry()
1704
log_formatter_registry.register('short', ShortLogFormatter,
1705
'Moderately short log format')
1706
log_formatter_registry.register('long', LongLogFormatter,
1707
'Detailed log format')
1708
log_formatter_registry.register('line', LineLogFormatter,
1709
'Log format with one line per revision')
1710
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1711
'Format used by GNU ChangeLog files')
1714
def register_formatter(name, formatter):
1715
log_formatter_registry.register(name, formatter)
1718
def log_formatter(name, *args, **kwargs):
1719
"""Construct a formatter from arguments.
1721
name -- Name of the formatter to construct; currently 'long', 'short' and
1722
'line' are supported.
1725
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1727
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1730
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1731
# deprecated; for compatibility
1732
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1733
lf.show(revno, rev, delta)
1736
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1738
"""Show the change in revision history comparing the old revision history to the new one.
1740
:param branch: The branch where the revisions exist
1741
:param old_rh: The old revision history
1742
:param new_rh: The new revision history
1743
:param to_file: A file to write the results to. If None, stdout will be used
1746
to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1748
lf = log_formatter(log_format,
1751
show_timezone='original')
1753
# This is the first index which is different between
1756
for i in xrange(max(len(new_rh),
1758
if (len(new_rh) <= i
1760
or new_rh[i] != old_rh[i]):
1764
if base_idx is None:
1765
to_file.write('Nothing seems to have changed\n')
1767
## TODO: It might be nice to do something like show_log
1768
## and show the merged entries. But since this is the
1769
## removed revisions, it shouldn't be as important
1770
if base_idx < len(old_rh):
1771
to_file.write('*'*60)
1772
to_file.write('\nRemoved Revisions:\n')
1773
for i in range(base_idx, len(old_rh)):
1774
rev = branch.repository.get_revision(old_rh[i])
1775
lr = LogRevision(rev, i+1, 0, None)
1777
to_file.write('*'*60)
1778
to_file.write('\n\n')
1779
if base_idx < len(new_rh):
1780
to_file.write('Added Revisions:\n')
1785
direction='forward',
1786
start_revision=base_idx+1,
1787
end_revision=len(new_rh),
1791
def get_history_change(old_revision_id, new_revision_id, repository):
1792
"""Calculate the uncommon lefthand history between two revisions.
1794
:param old_revision_id: The original revision id.
1795
:param new_revision_id: The new revision id.
1796
:param repository: The repository to use for the calculation.
1798
return old_history, new_history
1801
old_revisions = set()
1803
new_revisions = set()
1804
new_iter = repository.iter_reverse_revision_history(new_revision_id)
1805
old_iter = repository.iter_reverse_revision_history(old_revision_id)
1806
stop_revision = None
1809
while do_new or do_old:
1812
new_revision = new_iter.next()
1813
except StopIteration:
1816
new_history.append(new_revision)
1817
new_revisions.add(new_revision)
1818
if new_revision in old_revisions:
1819
stop_revision = new_revision
1823
old_revision = old_iter.next()
1824
except StopIteration:
1827
old_history.append(old_revision)
1828
old_revisions.add(old_revision)
1829
if old_revision in new_revisions:
1830
stop_revision = old_revision
1832
new_history.reverse()
1833
old_history.reverse()
1834
if stop_revision is not None:
1835
new_history = new_history[new_history.index(stop_revision) + 1:]
1836
old_history = old_history[old_history.index(stop_revision) + 1:]
1837
return old_history, new_history
1840
def show_branch_change(branch, output, old_revno, old_revision_id):
1841
"""Show the changes made to a branch.
1843
:param branch: The branch to show changes about.
1844
:param output: A file-like object to write changes to.
1845
:param old_revno: The revno of the old tip.
1846
:param old_revision_id: The revision_id of the old tip.
1848
new_revno, new_revision_id = branch.last_revision_info()
1849
old_history, new_history = get_history_change(old_revision_id,
1852
if old_history == [] and new_history == []:
1853
output.write('Nothing seems to have changed\n')
1856
log_format = log_formatter_registry.get_default(branch)
1857
lf = log_format(show_ids=False, to_file=output, show_timezone='original')
1858
if old_history != []:
1859
output.write('*'*60)
1860
output.write('\nRemoved Revisions:\n')
1861
show_flat_log(branch.repository, old_history, old_revno, lf)
1862
output.write('*'*60)
1863
output.write('\n\n')
1864
if new_history != []:
1865
output.write('Added Revisions:\n')
1866
start_revno = new_revno - len(new_history) + 1
1867
show_log(branch, lf, None, verbose=False, direction='forward',
1868
start_revision=start_revno,)
1871
def show_flat_log(repository, history, last_revno, lf):
1872
"""Show a simple log of the specified history.
1874
:param repository: The repository to retrieve revisions from.
1875
:param history: A list of revision_ids indicating the lefthand history.
1876
:param last_revno: The revno of the last revision_id in the history.
1877
:param lf: The log formatter to use.
1879
start_revno = last_revno - len(history) + 1
1880
revisions = repository.get_revisions(history)
1881
for i, rev in enumerate(revisions):
1882
lr = LogRevision(rev, i + last_revno, 0, None)
1886
def _get_info_for_log_files(revisionspec_list, file_list):
1887
"""Find file-ids and kinds given a list of files and a revision range.
1889
We search for files at the end of the range. If not found there,
1890
we try the start of the range.
1892
:param revisionspec_list: revision range as parsed on the command line
1893
:param file_list: the list of paths given on the command line;
1894
the first of these can be a branch location or a file path,
1895
the remainder must be file paths
1896
:return: (branch, info_list, start_rev_info, end_rev_info) where
1897
info_list is a list of (relative_path, file_id, kind) tuples where
1898
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1899
branch will be read-locked.
1901
from builtins import _get_revision_range, safe_relpath_files
1902
tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1904
# XXX: It's damn messy converting a list of paths to relative paths when
1905
# those paths might be deleted ones, they might be on a case-insensitive
1906
# filesystem and/or they might be in silly locations (like another branch).
1907
# For example, what should "log bzr://branch/dir/file1 file2" do? (Is
1908
# file2 implicitly in the same dir as file1 or should its directory be
1909
# taken from the current tree somehow?) For now, this solves the common
1910
# case of running log in a nested directory, assuming paths beyond the
1911
# first one haven't been deleted ...
1913
relpaths = [path] + safe_relpath_files(tree, file_list[1:])
1915
relpaths = [path] + file_list[1:]
1917
start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
1919
if relpaths in ([], [u'']):
1920
return b, [], start_rev_info, end_rev_info
1921
if start_rev_info is None and end_rev_info is None:
1923
tree = b.basis_tree()
1926
file_id = tree.path2id(fp)
1927
kind = _get_kind_for_file_id(tree, file_id)
1929
# go back to when time began
1932
rev1 = b.get_rev_id(1)
1933
except errors.NoSuchRevision:
1938
tree1 = b.repository.revision_tree(rev1)
1940
file_id = tree1.path2id(fp)
1941
kind = _get_kind_for_file_id(tree1, file_id)
1942
info_list.append((fp, file_id, kind))
1944
elif start_rev_info == end_rev_info:
1945
# One revision given - file must exist in it
1946
tree = b.repository.revision_tree(end_rev_info.rev_id)
1948
file_id = tree.path2id(fp)
1949
kind = _get_kind_for_file_id(tree, file_id)
1950
info_list.append((fp, file_id, kind))
1953
# Revision range given. Get the file-id from the end tree.
1954
# If that fails, try the start tree.
1955
rev_id = end_rev_info.rev_id
1957
tree = b.basis_tree()
1959
tree = b.repository.revision_tree(rev_id)
1962
file_id = tree.path2id(fp)
1963
kind = _get_kind_for_file_id(tree, file_id)
1966
rev_id = start_rev_info.rev_id
1968
rev1 = b.get_rev_id(1)
1969
tree1 = b.repository.revision_tree(rev1)
1971
tree1 = b.repository.revision_tree(rev_id)
1972
file_id = tree1.path2id(fp)
1973
kind = _get_kind_for_file_id(tree1, file_id)
1974
info_list.append((fp, file_id, kind))
1975
return b, info_list, start_rev_info, end_rev_info
1978
def _get_kind_for_file_id(tree, file_id):
1979
"""Return the kind of a file-id or None if it doesn't exist."""
1980
if file_id is not None:
1981
return tree.kind(file_id)
1986
properties_handler_registry = registry.Registry()
1988
# Use the properties handlers to print out bug information if available
1989
def _bugs_properties_handler(revision):
1990
if revision.properties.has_key('bugs'):
1991
bug_lines = revision.properties['bugs'].split('\n')
1992
bug_rows = [line.split(' ', 1) for line in bug_lines]
1993
fixed_bug_urls = [row[0] for row in bug_rows if
1994
len(row) > 1 and row[1] == 'fixed']
1997
return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2000
properties_handler_registry.register('bugs_properties_handler',
2001
_bugs_properties_handler)
2004
# adapters which revision ids to log are filtered. When log is called, the
2005
# log_rev_iterator is adapted through each of these factory methods.
2006
# Plugins are welcome to mutate this list in any way they like - as long
2007
# as the overall behaviour is preserved. At this point there is no extensible
2008
# mechanism for getting parameters to each factory method, and until there is
2009
# this won't be considered a stable api.
2013
# read revision objects
2014
_make_revision_objects,
2015
# filter on log messages
2016
_make_search_filter,
2017
# generate deltas for things we will show