1
# Copyright (C) 2005, 2006, 2007 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 (
59
from warnings import (
63
from bzrlib.lazy_import import lazy_import
64
lazy_import(globals(), """
70
repository as _mod_repository,
71
revision as _mod_revision,
81
from bzrlib.osutils import (
83
get_terminal_encoding,
88
def find_touching_revisions(branch, file_id):
89
"""Yield a description of revisions which affect the file_id.
91
Each returned element is (revno, revision_id, description)
93
This is the list of revisions where the file is either added,
94
modified, renamed or deleted.
96
TODO: Perhaps some way to limit this to only particular revisions,
97
or to traverse a non-mainline set of revisions?
102
for revision_id in branch.revision_history():
103
this_inv = branch.repository.get_revision_inventory(revision_id)
104
if file_id in this_inv:
105
this_ie = this_inv[file_id]
106
this_path = this_inv.id2path(file_id)
108
this_ie = this_path = None
110
# now we know how it was last time, and how it is in this revision.
111
# are those two states effectively the same or not?
113
if not this_ie and not last_ie:
114
# not present in either
116
elif this_ie and not last_ie:
117
yield revno, revision_id, "added " + this_path
118
elif not this_ie and last_ie:
120
yield revno, revision_id, "deleted " + last_path
121
elif this_path != last_path:
122
yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
123
elif (this_ie.text_size != last_ie.text_size
124
or this_ie.text_sha1 != last_ie.text_sha1):
125
yield revno, revision_id, "modified " + this_path
128
last_path = this_path
132
def _enumerate_history(branch):
135
for rev_id in branch.revision_history():
136
rh.append((revno, rev_id))
143
specific_fileid=None,
151
"""Write out human-readable log of commits to this branch.
153
:param lf: The LogFormatter object showing the output.
155
:param specific_fileid: If not None, list only the commits affecting the
156
specified file, rather than all commits.
158
:param verbose: If True show added/changed/deleted/renamed files.
160
:param direction: 'reverse' (default) is latest to earliest; 'forward' is
163
:param start_revision: If not None, only show revisions >= start_revision
165
:param end_revision: If not None, only show revisions <= end_revision
167
:param search: If not None, only show revisions with matching commit
170
:param limit: If set, shows only 'limit' revisions, all revisions are shown
173
:param show_diff: If True, output a diff after each revision.
177
if getattr(lf, 'begin_log', None):
180
_show_log(branch, lf, specific_fileid, verbose, direction,
181
start_revision, end_revision, search, limit, show_diff)
183
if getattr(lf, 'end_log', None):
189
def _show_log(branch,
191
specific_fileid=None,
199
"""Worker function for show_log - see show_log."""
200
if not isinstance(lf, LogFormatter):
201
warn("not a LogFormatter instance: %r" % lf)
204
trace.mutter('get log for file_id %r', specific_fileid)
205
levels_to_display = lf.get_levels()
206
generate_merge_revisions = levels_to_display != 1
207
allow_single_merge_revision = True
208
if not getattr(lf, 'supports_merge_revisions', False):
209
allow_single_merge_revision = getattr(lf,
210
'supports_single_merge_revision', False)
211
view_revisions = calculate_view_revisions(branch, start_revision,
212
end_revision, direction,
214
generate_merge_revisions,
215
allow_single_merge_revision)
217
generate_tags = getattr(lf, 'supports_tags', False)
219
if branch.supports_tags():
220
rev_tag_dict = branch.tags.get_reverse_tag_dict()
222
generate_delta = verbose and getattr(lf, 'supports_delta', False)
223
generate_diff = show_diff and getattr(lf, 'supports_diff', False)
225
# now we just print all the revisions
226
repo = branch.repository
228
revision_iterator = make_log_rev_iterator(branch, view_revisions,
229
generate_delta, search)
230
for revs in revision_iterator:
231
for (rev_id, revno, merge_depth), rev, delta in revs:
232
# Note: 0 levels means show everything; merge_depth counts from 0
233
if levels_to_display != 0 and merge_depth >= levels_to_display:
236
diff = _format_diff(repo, rev, rev_id, specific_fileid)
239
lr = LogRevision(rev, revno, merge_depth, delta,
240
rev_tag_dict.get(rev_id), diff)
244
if log_count >= limit:
248
def _format_diff(repo, rev, rev_id, specific_fileid):
249
if len(rev.parent_ids) == 0:
250
ancestor_id = _mod_revision.NULL_REVISION
252
ancestor_id = rev.parent_ids[0]
253
tree_1 = repo.revision_tree(ancestor_id)
254
tree_2 = repo.revision_tree(rev_id)
256
specific_files = [tree_2.id2path(specific_fileid)]
258
specific_files = None
260
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
265
def calculate_view_revisions(branch, start_revision, end_revision, direction,
266
specific_fileid, generate_merge_revisions,
267
allow_single_merge_revision):
268
if ( not generate_merge_revisions
269
and start_revision is end_revision is None
270
and direction == 'reverse'
271
and specific_fileid is None):
272
return _linear_view_revisions(branch)
274
mainline_revs, rev_nos, start_rev_id, end_rev_id = _get_mainline_revs(
275
branch, start_revision, end_revision)
276
if not mainline_revs:
279
generate_single_revision = False
280
if ((not generate_merge_revisions)
281
and ((start_rev_id and (start_rev_id not in rev_nos))
282
or (end_rev_id and (end_rev_id not in rev_nos)))):
283
generate_single_revision = ((start_rev_id == end_rev_id)
284
and allow_single_merge_revision)
285
if not generate_single_revision:
286
raise errors.BzrCommandError('Selected log formatter only supports'
287
' mainline revisions.')
288
generate_merge_revisions = generate_single_revision
289
include_merges = generate_merge_revisions or specific_fileid
290
view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
291
direction, include_merges=include_merges)
293
if direction == 'reverse':
294
start_rev_id, end_rev_id = end_rev_id, start_rev_id
295
view_revisions = _filter_revision_range(list(view_revs_iter),
298
if view_revisions and generate_single_revision:
299
view_revisions = view_revisions[0:1]
301
view_revisions = _filter_revisions_touching_file_id(branch,
302
specific_fileid, view_revisions,
303
include_merges=generate_merge_revisions)
305
# rebase merge_depth - unless there are no revisions or
306
# either the first or last revision have merge_depth = 0.
307
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
308
min_depth = min([d for r,n,d in view_revisions])
310
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
311
return view_revisions
314
def _linear_view_revisions(branch):
315
start_revno, start_revision_id = branch.last_revision_info()
316
repo = branch.repository
317
revision_ids = repo.iter_reverse_revision_history(start_revision_id)
318
for num, revision_id in enumerate(revision_ids):
319
yield revision_id, str(start_revno - num), 0
322
def make_log_rev_iterator(branch, view_revisions, generate_delta, search):
323
"""Create a revision iterator for log.
325
:param branch: The branch being logged.
326
:param view_revisions: The revisions being viewed.
327
:param generate_delta: Whether to generate a delta for each revision.
328
:param search: A user text search string.
329
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
332
# Convert view_revisions into (view, None, None) groups to fit with
333
# the standard interface here.
334
if type(view_revisions) == list:
335
# A single batch conversion is faster than many incremental ones.
336
# As we have all the data, do a batch conversion.
337
nones = [None] * len(view_revisions)
338
log_rev_iterator = iter([zip(view_revisions, nones, nones)])
341
for view in view_revisions:
342
yield (view, None, None)
343
log_rev_iterator = iter([_convert()])
344
for adapter in log_adapters:
345
log_rev_iterator = adapter(branch, generate_delta, search,
347
return log_rev_iterator
350
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
351
"""Create a filtered iterator of log_rev_iterator matching on a regex.
353
:param branch: The branch being logged.
354
:param generate_delta: Whether to generate a delta for each revision.
355
:param search: A user text search string.
356
:param log_rev_iterator: An input iterator containing all revisions that
357
could be displayed, in lists.
358
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
362
return log_rev_iterator
363
# Compile the search now to get early errors.
364
searchRE = re.compile(search, re.IGNORECASE)
365
return _filter_message_re(searchRE, log_rev_iterator)
368
def _filter_message_re(searchRE, log_rev_iterator):
369
for revs in log_rev_iterator:
371
for (rev_id, revno, merge_depth), rev, delta in revs:
372
if searchRE.search(rev.message):
373
new_revs.append(((rev_id, revno, merge_depth), rev, delta))
377
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator):
378
"""Add revision deltas to a log iterator if needed.
380
:param branch: The branch being logged.
381
:param generate_delta: Whether to generate a delta for each revision.
382
:param search: A user text search string.
383
:param log_rev_iterator: An input iterator containing all revisions that
384
could be displayed, in lists.
385
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
388
if not generate_delta:
389
return log_rev_iterator
390
return _generate_deltas(branch.repository, log_rev_iterator)
393
def _generate_deltas(repository, log_rev_iterator):
394
"""Create deltas for each batch of revisions in log_rev_iterator."""
395
for revs in log_rev_iterator:
396
revisions = [rev[1] for rev in revs]
397
deltas = repository.get_deltas_for_revisions(revisions)
398
revs = [(rev[0], rev[1], delta) for rev, delta in izip(revs, deltas)]
402
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
403
"""Extract revision objects from the repository
405
:param branch: The branch being logged.
406
:param generate_delta: Whether to generate a delta for each revision.
407
:param search: A user text search string.
408
:param log_rev_iterator: An input iterator containing all revisions that
409
could be displayed, in lists.
410
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
413
repository = branch.repository
414
for revs in log_rev_iterator:
415
# r = revision_id, n = revno, d = merge depth
416
revision_ids = [view[0] for view, _, _ in revs]
417
revisions = repository.get_revisions(revision_ids)
418
revs = [(rev[0], revision, rev[2]) for rev, revision in
419
izip(revs, revisions)]
423
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
424
"""Group up a single large batch into smaller ones.
426
:param branch: The branch being logged.
427
:param generate_delta: Whether to generate a delta for each revision.
428
:param search: A user text search string.
429
:param log_rev_iterator: An input iterator containing all revisions that
430
could be displayed, in lists.
431
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
434
repository = branch.repository
436
for batch in log_rev_iterator:
439
step = [detail for _, detail in zip(range(num), batch)]
443
num = min(int(num * 1.5), 200)
446
def _get_mainline_revs(branch, start_revision, end_revision):
447
"""Get the mainline revisions from the branch.
449
Generates the list of mainline revisions for the branch.
451
:param branch: The branch containing the revisions.
453
:param start_revision: The first revision to be logged.
454
For backwards compatibility this may be a mainline integer revno,
455
but for merge revision support a RevisionInfo is expected.
457
:param end_revision: The last revision to be logged.
458
For backwards compatibility this may be a mainline integer revno,
459
but for merge revision support a RevisionInfo is expected.
461
:return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
463
branch_revno, branch_last_revision = branch.last_revision_info()
464
if branch_revno == 0:
465
return None, None, None, None
467
# For mainline generation, map start_revision and end_revision to
468
# mainline revnos. If the revision is not on the mainline choose the
469
# appropriate extreme of the mainline instead - the extra will be
471
# Also map the revisions to rev_ids, to be used in the later filtering
474
if start_revision is None:
477
if isinstance(start_revision, revisionspec.RevisionInfo):
478
start_rev_id = start_revision.rev_id
479
start_revno = start_revision.revno or 1
481
branch.check_real_revno(start_revision)
482
start_revno = start_revision
485
if end_revision is None:
486
end_revno = branch_revno
488
if isinstance(end_revision, revisionspec.RevisionInfo):
489
end_rev_id = end_revision.rev_id
490
end_revno = end_revision.revno or branch_revno
492
branch.check_real_revno(end_revision)
493
end_revno = end_revision
495
if ((start_rev_id == _mod_revision.NULL_REVISION)
496
or (end_rev_id == _mod_revision.NULL_REVISION)):
497
raise errors.BzrCommandError('Logging revision 0 is invalid.')
498
if start_revno > end_revno:
499
raise errors.BzrCommandError("Start revision must be older than "
502
if end_revno < start_revno:
503
return None, None, None, None
504
cur_revno = branch_revno
507
for revision_id in branch.repository.iter_reverse_revision_history(
508
branch_last_revision):
509
if cur_revno < start_revno:
510
# We have gone far enough, but we always add 1 more revision
511
rev_nos[revision_id] = cur_revno
512
mainline_revs.append(revision_id)
514
if cur_revno <= end_revno:
515
rev_nos[revision_id] = cur_revno
516
mainline_revs.append(revision_id)
519
# We walked off the edge of all revisions, so we add a 'None' marker
520
mainline_revs.append(None)
522
mainline_revs.reverse()
524
# override the mainline to look like the revision history.
525
return mainline_revs, rev_nos, start_rev_id, end_rev_id
528
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
529
"""Filter view_revisions based on revision ranges.
531
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
532
tuples to be filtered.
534
:param start_rev_id: If not NONE specifies the first revision to be logged.
535
If NONE then all revisions up to the end_rev_id are logged.
537
:param end_rev_id: If not NONE specifies the last revision to be logged.
538
If NONE then all revisions up to the end of the log are logged.
540
:return: The filtered view_revisions.
542
if start_rev_id or end_rev_id:
543
revision_ids = [r for r, n, d in view_revisions]
545
start_index = revision_ids.index(start_rev_id)
548
if start_rev_id == end_rev_id:
549
end_index = start_index
552
end_index = revision_ids.index(end_rev_id)
554
end_index = len(view_revisions) - 1
555
# To include the revisions merged into the last revision,
556
# extend end_rev_id down to, but not including, the next rev
557
# with the same or lesser merge_depth
558
end_merge_depth = view_revisions[end_index][2]
560
for index in xrange(end_index+1, len(view_revisions)+1):
561
if view_revisions[index][2] <= end_merge_depth:
562
end_index = index - 1
565
# if the search falls off the end then log to the end as well
566
end_index = len(view_revisions) - 1
567
view_revisions = view_revisions[start_index:end_index+1]
568
return view_revisions
571
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
572
include_merges=True):
573
r"""Return the list of revision ids which touch a given file id.
575
The function filters view_revisions and returns a subset.
576
This includes the revisions which directly change the file id,
577
and the revisions which merge these changes. So if the
589
And 'C' changes a file, then both C and D will be returned. F will not be
590
returned even though it brings the changes to C into the branch starting
591
with E. (Note that if we were using F as the tip instead of G, then we
594
This will also be restricted based on a subset of the mainline.
596
:param branch: The branch where we can get text revision information.
598
:param file_id: Filter out revisions that do not touch file_id.
600
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
601
tuples. This is the list of revisions which will be filtered. It is
602
assumed that view_revisions is in merge_sort order (i.e. newest
605
:param include_merges: include merge revisions in the result or not
607
:return: A list of (revision_id, dotted_revno, merge_depth) tuples.
609
# Lookup all possible text keys to determine which ones actually modified
611
text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
612
# Looking up keys in batches of 1000 can cut the time in half, as well as
613
# memory consumption. GraphIndex *does* like to look for a few keys in
614
# parallel, it just doesn't like looking for *lots* of keys in parallel.
615
# TODO: This code needs to be re-evaluated periodically as we tune the
616
# indexing layer. We might consider passing in hints as to the known
617
# access pattern (sparse/clustered, high success rate/low success
618
# rate). This particular access is clustered with a low success rate.
619
get_parent_map = branch.repository.texts.get_parent_map
620
modified_text_revisions = set()
622
for start in xrange(0, len(text_keys), chunk_size):
623
next_keys = text_keys[start:start + chunk_size]
624
# Only keep the revision_id portion of the key
625
modified_text_revisions.update(
626
[k[1] for k in get_parent_map(next_keys)])
627
del text_keys, next_keys
630
# Track what revisions will merge the current revision, replace entries
631
# with 'None' when they have been added to result
632
current_merge_stack = [None]
633
for info in view_revisions:
634
rev_id, revno, depth = info
635
if depth == len(current_merge_stack):
636
current_merge_stack.append(info)
638
del current_merge_stack[depth + 1:]
639
current_merge_stack[-1] = info
641
if rev_id in modified_text_revisions:
642
# This needs to be logged, along with the extra revisions
643
for idx in xrange(len(current_merge_stack)):
644
node = current_merge_stack[idx]
646
if include_merges or node[2] == 0:
648
current_merge_stack[idx] = None
652
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
653
include_merges=True):
654
"""Produce an iterator of revisions to show
655
:return: an iterator of (revision_id, revno, merge_depth)
656
(if there is no revno for a revision, None is supplied)
658
if not include_merges:
659
revision_ids = mainline_revs[1:]
660
if direction == 'reverse':
661
revision_ids.reverse()
662
for revision_id in revision_ids:
663
yield revision_id, str(rev_nos[revision_id]), 0
665
graph = branch.repository.get_graph()
666
# This asks for all mainline revisions, which means we only have to spider
667
# sideways, rather than depth history. That said, its still size-of-history
668
# and should be addressed.
669
# mainline_revisions always includes an extra revision at the beginning, so
671
parent_map = dict(((key, value) for key, value in
672
graph.iter_ancestry(mainline_revs[1:]) if value is not None))
673
# filter out ghosts; merge_sort errors on ghosts.
674
rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
675
merge_sorted_revisions = tsort.merge_sort(
681
if direction == 'forward':
682
# forward means oldest first.
683
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
684
elif direction != 'reverse':
685
raise ValueError('invalid direction %r' % direction)
687
for (sequence, rev_id, merge_depth, revno, end_of_merge
688
) in merge_sorted_revisions:
689
yield rev_id, '.'.join(map(str, revno)), merge_depth
692
def reverse_by_depth(merge_sorted_revisions, _depth=0):
693
"""Reverse revisions by depth.
695
Revisions with a different depth are sorted as a group with the previous
696
revision of that depth. There may be no topological justification for this,
697
but it looks much nicer.
699
# Add a fake revision at start so that we can always attach sub revisions
700
merge_sorted_revisions = [(None, None, _depth)] + merge_sorted_revisions
702
for val in merge_sorted_revisions:
704
# Each revision at the current depth becomes a chunk grouping all
705
# higher depth revisions.
706
zd_revisions.append([val])
708
zd_revisions[-1].append(val)
709
for revisions in zd_revisions:
710
if len(revisions) > 1:
711
# We have higher depth revisions, let reverse them locally
712
revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
713
zd_revisions.reverse()
715
for chunk in zd_revisions:
718
# Top level call, get rid of the fake revisions that have been added
719
result = [r for r in result if r[0] is not None and r[1] is not None]
723
class LogRevision(object):
724
"""A revision to be logged (by LogFormatter.log_revision).
726
A simple wrapper for the attributes of a revision to be logged.
727
The attributes may or may not be populated, as determined by the
728
logging options and the log formatter capabilities.
731
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
732
tags=None, diff=None):
735
self.merge_depth = merge_depth
741
class LogFormatter(object):
742
"""Abstract class to display log messages.
744
At a minimum, a derived class must implement the log_revision method.
746
If the LogFormatter needs to be informed of the beginning or end of
747
a log it should implement the begin_log and/or end_log hook methods.
749
A LogFormatter should define the following supports_XXX flags
750
to indicate which LogRevision attributes it supports:
752
- supports_delta must be True if this log formatter supports delta.
753
Otherwise the delta attribute may not be populated. The 'delta_format'
754
attribute describes whether the 'short_status' format (1) or the long
755
one (2) sould be used.
757
- supports_merge_revisions must be True if this log formatter supports
758
merge revisions. If not, and if supports_single_merge_revisions is
759
also not True, then only mainline revisions will be passed to the
762
- preferred_levels is the number of levels this formatter defaults to.
763
The default value is zero meaning display all levels.
764
This value is only relevant if supports_merge_revisions is True.
766
- supports_single_merge_revision must be True if this log formatter
767
supports logging only a single merge revision. This flag is
768
only relevant if supports_merge_revisions is not True.
770
- supports_tags must be True if this log formatter supports tags.
771
Otherwise the tags attribute may not be populated.
773
- supports_diff must be True if this log formatter supports diffs.
774
Otherwise the diff attribute may not be populated.
776
Plugins can register functions to show custom revision properties using
777
the properties_handler_registry. The registered function
778
must respect the following interface description:
779
def my_show_properties(properties_dict):
780
# code that returns a dict {'name':'value'} of the properties
785
def __init__(self, to_file, show_ids=False, show_timezone='original',
786
delta_format=None, levels=None):
787
"""Create a LogFormatter.
789
:param to_file: the file to output to
790
:param show_ids: if True, revision-ids are to be displayed
791
:param show_timezone: the timezone to use
792
:param delta_format: the level of delta information to display
793
or None to leave it u to the formatter to decide
794
:param levels: the number of levels to display; None or -1 to
795
let the log formatter decide.
797
self.to_file = to_file
798
self.show_ids = show_ids
799
self.show_timezone = show_timezone
800
if delta_format is None:
801
# Ensures backward compatibility
802
delta_format = 2 # long format
803
self.delta_format = delta_format
806
def get_levels(self):
807
"""Get the number of levels to display or 0 for all."""
808
if getattr(self, 'supports_merge_revisions', False):
809
if self.levels is None or self.levels == -1:
810
return self.preferred_levels
815
def log_revision(self, revision):
818
:param revision: The LogRevision to be logged.
820
raise NotImplementedError('not implemented in abstract base')
822
def short_committer(self, rev):
823
name, address = config.parse_username(rev.committer)
828
def short_author(self, rev):
829
name, address = config.parse_username(rev.get_apparent_author())
834
def show_properties(self, revision, indent):
835
"""Displays the custom properties returned by each registered handler.
837
If a registered handler raises an error it is propagated.
839
for key, handler in properties_handler_registry.iteritems():
840
for key, value in handler(revision).items():
841
self.to_file.write(indent + key + ': ' + value + '\n')
843
def show_diff(self, to_file, diff, indent):
844
for l in diff.rstrip().split('\n'):
845
to_file.write(indent + '%s\n' % (l,))
848
class LongLogFormatter(LogFormatter):
850
supports_merge_revisions = True
851
supports_delta = True
855
def log_revision(self, revision):
856
"""Log a revision, either merged or not."""
857
indent = ' ' * revision.merge_depth
858
to_file = self.to_file
859
to_file.write(indent + '-' * 60 + '\n')
860
if revision.revno is not None:
861
to_file.write(indent + 'revno: %s\n' % (revision.revno,))
863
to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
865
to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
867
for parent_id in revision.rev.parent_ids:
868
to_file.write(indent + 'parent: %s\n' % (parent_id,))
869
self.show_properties(revision.rev, indent)
871
author = revision.rev.properties.get('author', None)
872
if author is not None:
873
to_file.write(indent + 'author: %s\n' % (author,))
874
to_file.write(indent + 'committer: %s\n' % (revision.rev.committer,))
876
branch_nick = revision.rev.properties.get('branch-nick', None)
877
if branch_nick is not None:
878
to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
880
date_str = format_date(revision.rev.timestamp,
881
revision.rev.timezone or 0,
883
to_file.write(indent + 'timestamp: %s\n' % (date_str,))
885
to_file.write(indent + 'message:\n')
886
if not revision.rev.message:
887
to_file.write(indent + ' (no message)\n')
889
message = revision.rev.message.rstrip('\r\n')
890
for l in message.split('\n'):
891
to_file.write(indent + ' %s\n' % (l,))
892
if revision.delta is not None:
893
# We don't respect delta_format for compatibility
894
revision.delta.show(to_file, self.show_ids, indent=indent,
896
if revision.diff is not None:
897
to_file.write(indent + 'diff:\n')
898
# Note: we explicitly don't indent the diff (relative to the
899
# revision information) so that the output can be fed to patch -p0
900
self.show_diff(to_file, revision.diff, indent)
903
class ShortLogFormatter(LogFormatter):
905
supports_merge_revisions = True
907
supports_delta = True
911
def __init__(self, *args, **kwargs):
912
super(ShortLogFormatter, self).__init__(*args, **kwargs)
913
self.revno_width_by_depth = {}
915
def log_revision(self, revision):
916
# We need two indents: one per depth and one for the information
917
# relative to that indent. Most mainline revnos are 5 chars or
918
# less while dotted revnos are typically 11 chars or less. Once
919
# calculated, we need to remember the offset for a given depth
920
# as we might be starting from a dotted revno in the first column
921
# and we want subsequent mainline revisions to line up.
922
depth = revision.merge_depth
924
revno_width = self.revno_width_by_depth.get(depth)
925
if revno_width is None:
926
if revision.revno.find('.') == -1:
927
# mainline revno, e.g. 12345
930
# dotted revno, e.g. 12345.10.55
932
self.revno_width_by_depth[depth] = revno_width
933
offset = ' ' * (revno_width + 1)
935
to_file = self.to_file
937
if len(revision.rev.parent_ids) > 1:
938
is_merge = ' [merge]'
941
tags = ' {%s}' % (', '.join(revision.tags))
942
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
943
revision.revno, self.short_author(revision.rev),
944
format_date(revision.rev.timestamp,
945
revision.rev.timezone or 0,
946
self.show_timezone, date_fmt="%Y-%m-%d",
950
to_file.write(indent + offset + 'revision-id:%s\n'
951
% (revision.rev.revision_id,))
952
if not revision.rev.message:
953
to_file.write(indent + offset + '(no message)\n')
955
message = revision.rev.message.rstrip('\r\n')
956
for l in message.split('\n'):
957
to_file.write(indent + offset + '%s\n' % (l,))
959
if revision.delta is not None:
960
revision.delta.show(to_file, self.show_ids, indent=indent + offset,
961
short_status=self.delta_format==1)
962
if revision.diff is not None:
963
self.show_diff(to_file, revision.diff, ' ')
967
class LineLogFormatter(LogFormatter):
969
supports_merge_revisions = True
973
def __init__(self, *args, **kwargs):
974
super(LineLogFormatter, self).__init__(*args, **kwargs)
975
self._max_chars = terminal_width() - 1
977
def truncate(self, str, max_len):
978
if len(str) <= max_len:
980
return str[:max_len-3]+'...'
982
def date_string(self, rev):
983
return format_date(rev.timestamp, rev.timezone or 0,
984
self.show_timezone, date_fmt="%Y-%m-%d",
987
def message(self, rev):
989
return '(no message)'
993
def log_revision(self, revision):
994
indent = ' ' * revision.merge_depth
995
self.to_file.write(self.log_string(revision.revno, revision.rev,
996
self._max_chars, revision.tags, indent))
997
self.to_file.write('\n')
999
def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1000
"""Format log info into one string. Truncate tail of string
1001
:param revno: revision number or None.
1002
Revision numbers counts from 1.
1003
:param rev: revision object
1004
:param max_chars: maximum length of resulting string
1005
:param tags: list of tags or None
1006
:param prefix: string to prefix each line
1007
:return: formatted truncated string
1011
# show revno only when is not None
1012
out.append("%s:" % revno)
1013
out.append(self.truncate(self.short_author(rev), 20))
1014
out.append(self.date_string(rev))
1016
tag_str = '{%s}' % (', '.join(tags))
1018
out.append(rev.get_summary())
1019
return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1022
def line_log(rev, max_chars):
1023
lf = LineLogFormatter(None)
1024
return lf.log_string(None, rev, max_chars)
1027
class LogFormatterRegistry(registry.Registry):
1028
"""Registry for log formatters"""
1030
def make_formatter(self, name, *args, **kwargs):
1031
"""Construct a formatter from arguments.
1033
:param name: Name of the formatter to construct. 'short', 'long' and
1034
'line' are built-in.
1036
return self.get(name)(*args, **kwargs)
1038
def get_default(self, branch):
1039
return self.get(branch.get_config().log_format())
1042
log_formatter_registry = LogFormatterRegistry()
1045
log_formatter_registry.register('short', ShortLogFormatter,
1046
'Moderately short log format')
1047
log_formatter_registry.register('long', LongLogFormatter,
1048
'Detailed log format')
1049
log_formatter_registry.register('line', LineLogFormatter,
1050
'Log format with one line per revision')
1053
def register_formatter(name, formatter):
1054
log_formatter_registry.register(name, formatter)
1057
def log_formatter(name, *args, **kwargs):
1058
"""Construct a formatter from arguments.
1060
name -- Name of the formatter to construct; currently 'long', 'short' and
1061
'line' are supported.
1064
return log_formatter_registry.make_formatter(name, *args, **kwargs)
1066
raise errors.BzrCommandError("unknown log formatter: %r" % name)
1069
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1070
# deprecated; for compatibility
1071
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1072
lf.show(revno, rev, delta)
1075
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1077
"""Show the change in revision history comparing the old revision history to the new one.
1079
:param branch: The branch where the revisions exist
1080
:param old_rh: The old revision history
1081
:param new_rh: The new revision history
1082
:param to_file: A file to write the results to. If None, stdout will be used
1085
to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1087
lf = log_formatter(log_format,
1090
show_timezone='original')
1092
# This is the first index which is different between
1095
for i in xrange(max(len(new_rh),
1097
if (len(new_rh) <= i
1099
or new_rh[i] != old_rh[i]):
1103
if base_idx is None:
1104
to_file.write('Nothing seems to have changed\n')
1106
## TODO: It might be nice to do something like show_log
1107
## and show the merged entries. But since this is the
1108
## removed revisions, it shouldn't be as important
1109
if base_idx < len(old_rh):
1110
to_file.write('*'*60)
1111
to_file.write('\nRemoved Revisions:\n')
1112
for i in range(base_idx, len(old_rh)):
1113
rev = branch.repository.get_revision(old_rh[i])
1114
lr = LogRevision(rev, i+1, 0, None)
1116
to_file.write('*'*60)
1117
to_file.write('\n\n')
1118
if base_idx < len(new_rh):
1119
to_file.write('Added Revisions:\n')
1124
direction='forward',
1125
start_revision=base_idx+1,
1126
end_revision=len(new_rh),
1130
def get_history_change(old_revision_id, new_revision_id, repository):
1131
"""Calculate the uncommon lefthand history between two revisions.
1133
:param old_revision_id: The original revision id.
1134
:param new_revision_id: The new revision id.
1135
:param repository: The repository to use for the calculation.
1137
return old_history, new_history
1140
old_revisions = set()
1142
new_revisions = set()
1143
new_iter = repository.iter_reverse_revision_history(new_revision_id)
1144
old_iter = repository.iter_reverse_revision_history(old_revision_id)
1145
stop_revision = None
1148
while do_new or do_old:
1151
new_revision = new_iter.next()
1152
except StopIteration:
1155
new_history.append(new_revision)
1156
new_revisions.add(new_revision)
1157
if new_revision in old_revisions:
1158
stop_revision = new_revision
1162
old_revision = old_iter.next()
1163
except StopIteration:
1166
old_history.append(old_revision)
1167
old_revisions.add(old_revision)
1168
if old_revision in new_revisions:
1169
stop_revision = old_revision
1171
new_history.reverse()
1172
old_history.reverse()
1173
if stop_revision is not None:
1174
new_history = new_history[new_history.index(stop_revision) + 1:]
1175
old_history = old_history[old_history.index(stop_revision) + 1:]
1176
return old_history, new_history
1179
def show_branch_change(branch, output, old_revno, old_revision_id):
1180
"""Show the changes made to a branch.
1182
:param branch: The branch to show changes about.
1183
:param output: A file-like object to write changes to.
1184
:param old_revno: The revno of the old tip.
1185
:param old_revision_id: The revision_id of the old tip.
1187
new_revno, new_revision_id = branch.last_revision_info()
1188
old_history, new_history = get_history_change(old_revision_id,
1191
if old_history == [] and new_history == []:
1192
output.write('Nothing seems to have changed\n')
1195
log_format = log_formatter_registry.get_default(branch)
1196
lf = log_format(show_ids=False, to_file=output, show_timezone='original')
1197
if old_history != []:
1198
output.write('*'*60)
1199
output.write('\nRemoved Revisions:\n')
1200
show_flat_log(branch.repository, old_history, old_revno, lf)
1201
output.write('*'*60)
1202
output.write('\n\n')
1203
if new_history != []:
1204
output.write('Added Revisions:\n')
1205
start_revno = new_revno - len(new_history) + 1
1206
show_log(branch, lf, None, verbose=False, direction='forward',
1207
start_revision=start_revno,)
1210
def show_flat_log(repository, history, last_revno, lf):
1211
"""Show a simple log of the specified history.
1213
:param repository: The repository to retrieve revisions from.
1214
:param history: A list of revision_ids indicating the lefthand history.
1215
:param last_revno: The revno of the last revision_id in the history.
1216
:param lf: The log formatter to use.
1218
start_revno = last_revno - len(history) + 1
1219
revisions = repository.get_revisions(history)
1220
for i, rev in enumerate(revisions):
1221
lr = LogRevision(rev, i + last_revno, 0, None)
1225
def _get_fileid_to_log(revision, tree, b, fp):
1226
"""Find the file-id to log for a file path in a revision range.
1228
:param revision: the revision range as parsed on the command line
1229
:param tree: the working tree, if any
1230
:param b: the branch
1231
:param fp: file path
1233
if revision is None:
1235
tree = b.basis_tree()
1236
file_id = tree.path2id(fp)
1238
# go back to when time began
1240
rev1 = b.get_rev_id(1)
1241
except errors.NoSuchRevision:
1245
tree = b.repository.revision_tree(rev1)
1246
file_id = tree.path2id(fp)
1248
elif len(revision) == 1:
1249
# One revision given - file must exist in it
1250
tree = revision[0].as_tree(b)
1251
file_id = tree.path2id(fp)
1253
elif len(revision) == 2:
1254
# Revision range given. Get the file-id from the end tree.
1255
# If that fails, try the start tree.
1256
rev_id = revision[1].as_revision_id(b)
1258
tree = b.basis_tree()
1260
tree = revision[1].as_tree(b)
1261
file_id = tree.path2id(fp)
1263
rev_id = revision[0].as_revision_id(b)
1265
rev1 = b.get_rev_id(1)
1266
tree = b.repository.revision_tree(rev1)
1268
tree = revision[0].as_tree(b)
1269
file_id = tree.path2id(fp)
1271
raise errors.BzrCommandError(
1272
'bzr log --revision takes one or two values.')
1276
properties_handler_registry = registry.Registry()
1277
properties_handler_registry.register_lazy("foreign",
1279
"show_foreign_properties")
1282
# adapters which revision ids to log are filtered. When log is called, the
1283
# log_rev_iterator is adapted through each of these factory methods.
1284
# Plugins are welcome to mutate this list in any way they like - as long
1285
# as the overall behaviour is preserved. At this point there is no extensible
1286
# mechanism for getting parameters to each factory method, and until there is
1287
# this won't be considered a stable api.
1291
# read revision objects
1292
_make_revision_objects,
1293
# filter on log messages
1294
_make_search_filter,
1295
# generate deltas for things we will show