194
159
direction='reverse',
195
160
start_revision=None,
196
161
end_revision=None,
199
163
"""Worker function for show_log - see show_log."""
164
from bzrlib.osutils import format_date
165
from bzrlib.errors import BzrCheckError
167
from warnings import warn
200
169
if not isinstance(lf, LogFormatter):
201
170
warn("not a LogFormatter instance: %r" % lf)
203
172
if specific_fileid:
204
trace.mutter('get log for file_id %r', specific_fileid)
205
generate_merge_revisions = getattr(lf, 'supports_merge_revisions', False)
206
allow_single_merge_revision = getattr(lf,
207
'supports_single_merge_revision', False)
208
view_revisions = calculate_view_revisions(branch, start_revision,
209
end_revision, direction,
211
generate_merge_revisions,
212
allow_single_merge_revision)
173
mutter('get log for file_id %r', specific_fileid)
213
175
if search is not None:
214
177
searchRE = re.compile(search, re.IGNORECASE)
219
generate_tags = getattr(lf, 'supports_tags', False)
221
if branch.supports_tags():
222
rev_tag_dict = branch.tags.get_reverse_tag_dict()
224
generate_delta = verbose and getattr(lf, 'supports_delta', False)
181
which_revs = _enumerate_history(branch)
183
if start_revision is None:
186
branch.check_real_revno(start_revision)
188
if end_revision is None:
189
end_revision = len(which_revs)
191
branch.check_real_revno(end_revision)
193
# list indexes are 0-based; revisions are 1-based
194
cut_revs = which_revs[(start_revision-1):(end_revision)]
198
# convert the revision history to a dictionary:
199
rev_nos = dict((k, v) for v, k in cut_revs)
201
# override the mainline to look like the revision history.
202
mainline_revs = [revision_id for index, revision_id in cut_revs]
203
if cut_revs[0][0] == 1:
204
mainline_revs.insert(0, None)
206
mainline_revs.insert(0, which_revs[start_revision-2][1])
207
# how should we show merged revisions ?
208
# old api: show_merge. New api: show_merge_revno
209
show_merge_revno = getattr(lf, 'show_merge_revno', None)
210
show_merge = getattr(lf, 'show_merge', None)
211
if show_merge is None and show_merge_revno is None:
212
# no merged-revno support
213
include_merges = False
215
include_merges = True
216
if show_merge is not None and show_merge_revno is None:
217
# tell developers to update their code
218
symbol_versioning.warn('LogFormatters should provide show_merge_revno '
219
'instead of show_merge since bzr 0.11.',
220
DeprecationWarning, stacklevel=3)
221
view_revisions = list(get_view_revisions(mainline_revs, rev_nos, branch,
222
direction, include_merges=include_merges))
224
def iter_revisions():
225
# r = revision, n = revno, d = merge depth
226
revision_ids = [r for r, n, d in view_revisions]
227
zeros = set(r for r, n, d in view_revisions if d == 0)
229
repository = branch.repository
232
revisions = repository.get_revisions(revision_ids[:num])
233
if verbose or specific_fileid:
234
delta_revisions = [r for r in revisions if
235
r.revision_id in zeros]
236
deltas = repository.get_deltas_for_revisions(delta_revisions)
237
cur_deltas = dict(izip((r.revision_id for r in
238
delta_revisions), deltas))
239
for revision in revisions:
240
# The delta value will be None unless
241
# 1. verbose or specific_fileid is specified, and
242
# 2. the revision is a mainline revision
243
yield revision, cur_deltas.get(revision.revision_id)
244
revision_ids = revision_ids[num:]
226
247
# now we just print all the revisions
228
for (rev_id, revno, merge_depth), rev, delta in _iter_revisions(
229
branch.repository, view_revisions, generate_delta):
248
for ((rev_id, revno, merge_depth), (rev, delta)) in \
249
izip(view_revisions, iter_revisions()):
231
252
if not searchRE.search(rev.message):
234
lr = LogRevision(rev, revno, merge_depth, delta,
235
rev_tag_dict.get(rev_id))
239
if log_count >= limit:
243
def calculate_view_revisions(branch, start_revision, end_revision, direction,
244
specific_fileid, generate_merge_revisions,
245
allow_single_merge_revision):
246
if (not generate_merge_revisions and start_revision is end_revision is
247
None and direction == 'reverse' and specific_fileid is None):
248
return _linear_view_revisions(branch)
250
mainline_revs, rev_nos, start_rev_id, end_rev_id = \
251
_get_mainline_revs(branch, start_revision, end_revision)
252
if not mainline_revs:
255
if direction == 'reverse':
256
start_rev_id, end_rev_id = end_rev_id, start_rev_id
258
generate_single_revision = False
259
if ((not generate_merge_revisions)
260
and ((start_rev_id and (start_rev_id not in rev_nos))
261
or (end_rev_id and (end_rev_id not in rev_nos)))):
262
generate_single_revision = ((start_rev_id == end_rev_id)
263
and allow_single_merge_revision)
264
if not generate_single_revision:
265
raise errors.BzrCommandError('Selected log formatter only supports'
266
' mainline revisions.')
267
generate_merge_revisions = generate_single_revision
268
view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
269
direction, include_merges=generate_merge_revisions)
270
view_revisions = _filter_revision_range(list(view_revs_iter),
273
if view_revisions and generate_single_revision:
274
view_revisions = view_revisions[0:1]
276
view_revisions = _filter_revisions_touching_file_id(branch,
281
# rebase merge_depth - unless there are no revisions or
282
# either the first or last revision have merge_depth = 0.
283
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
284
min_depth = min([d for r,n,d in view_revisions])
286
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
287
return view_revisions
290
def _linear_view_revisions(branch):
291
start_revno, start_revision_id = branch.last_revision_info()
292
repo = branch.repository
293
revision_ids = repo.iter_reverse_revision_history(start_revision_id)
294
for num, revision_id in enumerate(revision_ids):
295
yield revision_id, str(start_revno - num), 0
298
def _iter_revisions(repository, view_revisions, generate_delta):
300
view_revisions = iter(view_revisions)
302
cur_view_revisions = [d for x, d in zip(range(num), view_revisions)]
303
if len(cur_view_revisions) == 0:
306
# r = revision, n = revno, d = merge depth
307
revision_ids = [r for (r, n, d) in cur_view_revisions]
308
revisions = repository.get_revisions(revision_ids)
310
deltas = repository.get_deltas_for_revisions(revisions)
311
cur_deltas = dict(izip((r.revision_id for r in revisions),
313
for view_data, revision in izip(cur_view_revisions, revisions):
314
yield view_data, revision, cur_deltas.get(revision.revision_id)
315
num = min(int(num * 1.5), 200)
318
def _get_mainline_revs(branch, start_revision, end_revision):
319
"""Get the mainline revisions from the branch.
321
Generates the list of mainline revisions for the branch.
323
:param branch: The branch containing the revisions.
325
:param start_revision: The first revision to be logged.
326
For backwards compatibility this may be a mainline integer revno,
327
but for merge revision support a RevisionInfo is expected.
329
:param end_revision: The last revision to be logged.
330
For backwards compatibility this may be a mainline integer revno,
331
but for merge revision support a RevisionInfo is expected.
333
:return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
335
branch_revno, branch_last_revision = branch.last_revision_info()
336
if branch_revno == 0:
337
return None, None, None, None
339
# For mainline generation, map start_revision and end_revision to
340
# mainline revnos. If the revision is not on the mainline choose the
341
# appropriate extreme of the mainline instead - the extra will be
343
# Also map the revisions to rev_ids, to be used in the later filtering
346
if start_revision is None:
349
if isinstance(start_revision, revisionspec.RevisionInfo):
350
start_rev_id = start_revision.rev_id
351
start_revno = start_revision.revno or 1
353
branch.check_real_revno(start_revision)
354
start_revno = start_revision
357
if end_revision is None:
358
end_revno = branch_revno
360
if isinstance(end_revision, revisionspec.RevisionInfo):
361
end_rev_id = end_revision.rev_id
362
end_revno = end_revision.revno or branch_revno
364
branch.check_real_revno(end_revision)
365
end_revno = end_revision
367
if ((start_rev_id == _mod_revision.NULL_REVISION)
368
or (end_rev_id == _mod_revision.NULL_REVISION)):
369
raise errors.BzrCommandError('Logging revision 0 is invalid.')
370
if start_revno > end_revno:
371
raise errors.BzrCommandError("Start revision must be older than "
374
if end_revno < start_revno:
375
return None, None, None, None
376
cur_revno = branch_revno
379
for revision_id in branch.repository.iter_reverse_revision_history(
380
branch_last_revision):
381
if cur_revno < start_revno:
382
# We have gone far enough, but we always add 1 more revision
383
rev_nos[revision_id] = cur_revno
384
mainline_revs.append(revision_id)
386
if cur_revno <= end_revno:
387
rev_nos[revision_id] = cur_revno
388
mainline_revs.append(revision_id)
391
# We walked off the edge of all revisions, so we add a 'None' marker
392
mainline_revs.append(None)
394
mainline_revs.reverse()
396
# override the mainline to look like the revision history.
397
return mainline_revs, rev_nos, start_rev_id, end_rev_id
400
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
401
"""Filter view_revisions based on revision ranges.
403
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
404
tuples to be filtered.
406
:param start_rev_id: If not NONE specifies the first revision to be logged.
407
If NONE then all revisions up to the end_rev_id are logged.
409
:param end_rev_id: If not NONE specifies the last revision to be logged.
410
If NONE then all revisions up to the end of the log are logged.
412
:return: The filtered view_revisions.
414
if start_rev_id or end_rev_id:
415
revision_ids = [r for r, n, d in view_revisions]
417
start_index = revision_ids.index(start_rev_id)
420
if start_rev_id == end_rev_id:
421
end_index = start_index
424
end_index = revision_ids.index(end_rev_id)
256
# a mainline revision.
259
if not delta.touches_file_id(specific_fileid):
263
# although we calculated it, throw it away without display
266
lf.show(revno, rev, delta)
268
if show_merge_revno is None:
269
lf.show_merge(rev, merge_depth)
426
end_index = len(view_revisions) - 1
427
# To include the revisions merged into the last revision,
428
# extend end_rev_id down to, but not including, the next rev
429
# with the same or lesser merge_depth
430
end_merge_depth = view_revisions[end_index][2]
432
for index in xrange(end_index+1, len(view_revisions)+1):
433
if view_revisions[index][2] <= end_merge_depth:
434
end_index = index - 1
437
# if the search falls off the end then log to the end as well
438
end_index = len(view_revisions) - 1
439
view_revisions = view_revisions[start_index:end_index+1]
440
return view_revisions
443
def _filter_revisions_touching_file_id(branch, file_id, mainline_revisions,
445
"""Return the list of revision ids which touch a given file id.
447
The function filters view_revisions and returns a subset.
448
This includes the revisions which directly change the file id,
449
and the revisions which merge these changes. So if the
457
And 'C' changes a file, then both C and D will be returned.
459
This will also can be restricted based on a subset of the mainline.
461
:return: A list of (revision_id, dotted_revno, merge_depth) tuples.
463
# find all the revisions that change the specific file
464
# build the ancestry of each revision in the graph
465
# - only listing the ancestors that change the specific file.
466
graph = branch.repository.get_graph()
467
# This asks for all mainline revisions, which means we only have to spider
468
# sideways, rather than depth history. That said, its still size-of-history
469
# and should be addressed.
470
# mainline_revisions always includes an extra revision at the beginning, so
472
parent_map = dict(((key, value) for key, value in
473
graph.iter_ancestry(mainline_revisions[1:]) if value is not None))
474
sorted_rev_list = tsort.topo_sort(parent_map.items())
475
text_keys = [(file_id, rev_id) for rev_id in sorted_rev_list]
476
modified_text_versions = branch.repository.texts.get_parent_map(text_keys)
478
for rev in sorted_rev_list:
479
text_key = (file_id, rev)
480
parents = parent_map[rev]
481
if text_key not in modified_text_versions and len(parents) == 1:
482
# We will not be adding anything new, so just use a reference to
483
# the parent ancestry.
484
rev_ancestry = ancestry[parents[0]]
487
if text_key in modified_text_versions:
488
rev_ancestry.add(rev)
489
for parent in parents:
490
if parent not in ancestry:
491
# parent is a Ghost, which won't be present in
492
# sorted_rev_list, but we may access it later, so create an
494
ancestry[parent] = set()
495
rev_ancestry = rev_ancestry.union(ancestry[parent])
496
ancestry[rev] = rev_ancestry
498
def is_merging_rev(r):
499
parents = parent_map[r]
501
leftparent = parents[0]
502
for rightparent in parents[1:]:
503
if not ancestry[leftparent].issuperset(
504
ancestry[rightparent]):
508
# filter from the view the revisions that did not change or merge
510
return [(r, n, d) for r, n, d in view_revs_iter
511
if (file_id, r) in modified_text_versions or is_merging_rev(r)]
271
lf.show_merge_revno(rev, merge_depth, revno)
514
274
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
576
class LogRevision(object):
577
"""A revision to be logged (by LogFormatter.log_revision).
579
A simple wrapper for the attributes of a revision to be logged.
580
The attributes may or may not be populated, as determined by the
581
logging options and the log formatter capabilities.
584
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
588
self.merge_depth = merge_depth
593
327
class LogFormatter(object):
594
"""Abstract class to display log messages.
596
At a minimum, a derived class must implement the log_revision method.
598
If the LogFormatter needs to be informed of the beginning or end of
599
a log it should implement the begin_log and/or end_log hook methods.
601
A LogFormatter should define the following supports_XXX flags
602
to indicate which LogRevision attributes it supports:
604
- supports_delta must be True if this log formatter supports delta.
605
Otherwise the delta attribute may not be populated.
606
- supports_merge_revisions must be True if this log formatter supports
607
merge revisions. If not, and if supports_single_merge_revisions is
608
also not True, then only mainline revisions will be passed to the
610
- supports_single_merge_revision must be True if this log formatter
611
supports logging only a single merge revision. This flag is
612
only relevant if supports_merge_revisions is not True.
613
- supports_tags must be True if this log formatter supports tags.
614
Otherwise the tags attribute may not be populated.
616
Plugins can register functions to show custom revision properties using
617
the properties_handler_registry. The registered function
618
must respect the following interface description:
619
def my_show_properties(properties_dict):
620
# code that returns a dict {'name':'value'} of the properties
328
"""Abstract class to display log messages."""
624
330
def __init__(self, to_file, show_ids=False, show_timezone='original'):
625
331
self.to_file = to_file
626
332
self.show_ids = show_ids
627
333
self.show_timezone = show_timezone
629
# TODO: uncomment this block after show() has been removed.
630
# Until then defining log_revision would prevent _show_log calling show()
631
# in legacy formatters.
632
# def log_revision(self, revision):
635
# :param revision: The LogRevision to be logged.
637
# raise NotImplementedError('not implemented in abstract base')
335
def show(self, revno, rev, delta):
336
raise NotImplementedError('not implemented in abstract base')
639
338
def short_committer(self, rev):
640
name, address = config.parse_username(rev.committer)
645
def short_author(self, rev):
646
name, address = config.parse_username(rev.get_apparent_author())
651
def show_properties(self, revision, indent):
652
"""Displays the custom properties returned by each registered handler.
654
If a registered handler raises an error it is propagated.
656
for key, handler in properties_handler_registry.iteritems():
657
for key, value in handler(revision).items():
658
self.to_file.write(indent + key + ': ' + value + '\n')
339
return re.sub('<.*@.*>', '', rev.committer).strip(' ')
661
342
class LongLogFormatter(LogFormatter):
663
supports_merge_revisions = True
664
supports_delta = True
667
def log_revision(self, revision):
668
"""Log a revision, either merged or not."""
669
indent = ' ' * revision.merge_depth
343
def show(self, revno, rev, delta):
344
return self._show_helper(revno=revno, rev=rev, delta=delta)
346
@deprecated_method(zero_eleven)
347
def show_merge(self, rev, merge_depth):
348
return self._show_helper(rev=rev, indent=' '*merge_depth, merged=True, delta=None)
350
def show_merge_revno(self, rev, merge_depth, revno):
351
"""Show a merged revision rev, with merge_depth and a revno."""
352
return self._show_helper(rev=rev, revno=revno,
353
indent=' '*merge_depth, merged=True, delta=None)
355
def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
356
"""Show a revision, either merged or not."""
357
from bzrlib.osutils import format_date
670
358
to_file = self.to_file
671
to_file.write(indent + '-' * 60 + '\n')
672
if revision.revno is not None:
673
to_file.write(indent + 'revno: %s\n' % (revision.revno,))
675
to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
359
print >>to_file, indent+'-' * 60
360
if revno is not None:
361
print >>to_file, indent+'revno:', revno
363
print >>to_file, indent+'merged:', rev.revision_id
365
print >>to_file, indent+'revision-id:', rev.revision_id
676
366
if self.show_ids:
677
to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
679
for parent_id in revision.rev.parent_ids:
680
to_file.write(indent + 'parent: %s\n' % (parent_id,))
681
self.show_properties(revision.rev, indent)
683
author = revision.rev.properties.get('author', None)
684
if author is not None:
685
to_file.write(indent + 'author: %s\n' % (author,))
686
to_file.write(indent + 'committer: %s\n' % (revision.rev.committer,))
688
branch_nick = revision.rev.properties.get('branch-nick', None)
689
if branch_nick is not None:
690
to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
692
date_str = format_date(revision.rev.timestamp,
693
revision.rev.timezone or 0,
367
for parent_id in rev.parent_ids:
368
print >>to_file, indent+'parent:', parent_id
369
print >>to_file, indent+'committer:', rev.committer
371
print >>to_file, indent+'branch nick: %s' % \
372
rev.properties['branch-nick']
375
date_str = format_date(rev.timestamp,
694
377
self.show_timezone)
695
to_file.write(indent + 'timestamp: %s\n' % (date_str,))
378
print >>to_file, indent+'timestamp: %s' % date_str
697
to_file.write(indent + 'message:\n')
698
if not revision.rev.message:
699
to_file.write(indent + ' (no message)\n')
380
print >>to_file, indent+'message:'
382
print >>to_file, indent+' (no message)'
701
message = revision.rev.message.rstrip('\r\n')
384
message = rev.message.rstrip('\r\n')
702
385
for l in message.split('\n'):
703
to_file.write(indent + ' %s\n' % (l,))
704
if revision.delta is not None:
705
revision.delta.show(to_file, self.show_ids, indent=indent)
386
print >>to_file, indent+' ' + l
387
if delta is not None:
388
delta.show(to_file, self.show_ids)
708
391
class ShortLogFormatter(LogFormatter):
710
supports_delta = True
711
supports_single_merge_revision = True
713
def log_revision(self, revision):
392
def show(self, revno, rev, delta):
393
from bzrlib.osutils import format_date
714
395
to_file = self.to_file
716
if len(revision.rev.parent_ids) > 1:
717
is_merge = ' [merge]'
718
to_file.write("%5s %s\t%s%s\n" % (revision.revno,
719
self.short_author(revision.rev),
720
format_date(revision.rev.timestamp,
721
revision.rev.timezone or 0,
396
date_str = format_date(rev.timestamp, rev.timezone or 0,
398
print >>to_file, "%5s %s\t%s" % (revno, self.short_committer(rev),
399
format_date(rev.timestamp, rev.timezone or 0,
722
400
self.show_timezone, date_fmt="%Y-%m-%d",
725
402
if self.show_ids:
726
to_file.write(' revision-id:%s\n' % (revision.rev.revision_id,))
727
if not revision.rev.message:
728
to_file.write(' (no message)\n')
403
print >>to_file, ' revision-id:', rev.revision_id
405
print >>to_file, ' (no message)'
730
message = revision.rev.message.rstrip('\r\n')
407
message = rev.message.rstrip('\r\n')
731
408
for l in message.split('\n'):
732
to_file.write(' %s\n' % (l,))
409
print >>to_file, ' ' + l
734
411
# TODO: Why not show the modified files in a shorter form as
735
412
# well? rewrap them single lines of appropriate length
736
if revision.delta is not None:
737
revision.delta.show(to_file, self.show_ids)
413
if delta is not None:
414
delta.show(to_file, self.show_ids)
741
418
class LineLogFormatter(LogFormatter):
743
supports_single_merge_revision = True
745
def __init__(self, *args, **kwargs):
746
super(LineLogFormatter, self).__init__(*args, **kwargs)
747
self._max_chars = terminal_width() - 1
749
419
def truncate(self, str, max_len):
750
420
if len(str) <= max_len:
752
422
return str[:max_len-3]+'...'
754
424
def date_string(self, rev):
425
from bzrlib.osutils import format_date
755
426
return format_date(rev.timestamp, rev.timezone or 0,
756
427
self.show_timezone, date_fmt="%Y-%m-%d",
757
428
show_offset=False)