210
171
mutter('get log for file_id %r', specific_fileid)
212
173
if search is not None:
213
175
searchRE = re.compile(search, re.IGNORECASE)
217
mainline_revs, rev_nos, start_rev_id, end_rev_id = \
218
_get_mainline_revs(branch, start_revision, end_revision)
219
if not mainline_revs:
179
which_revs = _enumerate_history(branch)
181
if start_revision is None:
184
branch.check_real_revno(start_revision)
186
if end_revision is None:
187
end_revision = len(which_revs)
189
branch.check_real_revno(end_revision)
191
# list indexes are 0-based; revisions are 1-based
192
cut_revs = which_revs[(start_revision-1):(end_revision)]
222
if direction == 'reverse':
223
start_rev_id, end_rev_id = end_rev_id, start_rev_id
225
legacy_lf = getattr(lf, 'log_revision', None) is None
227
# pre-0.17 formatters use show for mainline revisions.
228
# how should we show merged revisions ?
229
# pre-0.11 api: show_merge
230
# 0.11-0.16 api: show_merge_revno
231
show_merge_revno = getattr(lf, 'show_merge_revno', None)
232
show_merge = getattr(lf, 'show_merge', None)
233
if show_merge is None and show_merge_revno is None:
234
# no merged-revno support
235
generate_merge_revisions = False
237
generate_merge_revisions = True
238
# tell developers to update their code
239
symbol_versioning.warn('LogFormatters should provide log_revision '
240
'instead of show and show_merge_revno since bzr 0.17.',
241
DeprecationWarning, stacklevel=3)
243
generate_merge_revisions = getattr(lf, 'supports_merge_revisions',
245
generate_single_revision = False
246
if ((not generate_merge_revisions)
247
and ((start_rev_id and (start_rev_id not in rev_nos))
248
or (end_rev_id and (end_rev_id not in rev_nos)))):
249
generate_single_revision = ((start_rev_id == end_rev_id)
250
and getattr(lf, 'supports_single_merge_revision', False))
251
if not generate_single_revision:
252
raise BzrCommandError('Selected log formatter only supports '
253
'mainline revisions.')
254
generate_merge_revisions = generate_single_revision
255
view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
256
direction, include_merges=generate_merge_revisions)
257
view_revisions = _filter_revision_range(list(view_revs_iter),
260
if view_revisions and generate_single_revision:
261
view_revisions = view_revisions[0:1]
263
view_revisions = _filter_revisions_touching_file_id(branch,
268
# rebase merge_depth - unless there are no revisions or
269
# either the first or last revision have merge_depth = 0.
270
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
271
min_depth = min([d for r,n,d in view_revisions])
273
view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
276
generate_tags = getattr(lf, 'supports_tags', False)
278
if branch.supports_tags():
279
rev_tag_dict = branch.tags.get_reverse_tag_dict()
281
generate_delta = verbose and getattr(lf, 'supports_delta', False)
196
# convert the revision history to a dictionary:
197
rev_nos = dict((k, v) for v, k in cut_revs)
199
# override the mainline to look like the revision history.
200
mainline_revs = [revision_id for index, revision_id in cut_revs]
201
if cut_revs[0][0] == 1:
202
mainline_revs.insert(0, None)
204
mainline_revs.insert(0, which_revs[start_revision-2][1])
205
if getattr(lf, 'show_merge', None) is not None:
206
include_merges = True
208
include_merges = False
209
view_revisions = list(get_view_revisions(mainline_revs, rev_nos, branch,
210
direction, include_merges=include_merges))
283
212
def iter_revisions():
284
213
# r = revision, n = revno, d = merge depth
285
214
revision_ids = [r for r, n, d in view_revisions]
215
zeros = set(r for r, n, d in view_revisions if d == 0)
287
217
repository = branch.repository
288
218
while revision_ids:
290
220
revisions = repository.get_revisions(revision_ids[:num])
292
deltas = repository.get_deltas_for_revisions(revisions)
293
cur_deltas = dict(izip((r.revision_id for r in revisions),
221
if verbose or specific_fileid:
222
delta_revisions = [r for r in revisions if
223
r.revision_id in zeros]
224
deltas = repository.get_deltas_for_revisions(delta_revisions)
225
cur_deltas = dict(izip((r.revision_id for r in
226
delta_revisions), deltas))
295
227
for revision in revisions:
228
# The delta value will be None unless
229
# 1. verbose or specific_fileid is specified, and
230
# 2. the revision is a mainline revision
296
231
yield revision, cur_deltas.get(revision.revision_id)
297
232
revision_ids = revision_ids[num:]
298
num = min(int(num * 1.5), 200)
300
235
# now we just print all the revisions
302
236
for ((rev_id, revno, merge_depth), (rev, delta)) in \
303
237
izip(view_revisions, iter_revisions()):
306
240
if not searchRE.search(rev.message):
310
lr = LogRevision(rev, revno, merge_depth, delta,
311
rev_tag_dict.get(rev_id))
314
# support for legacy (pre-0.17) LogFormatters
317
lf.show(revno, rev, delta, rev_tag_dict.get(rev_id))
319
lf.show(revno, rev, delta)
321
if show_merge_revno is None:
322
lf.show_merge(rev, merge_depth)
325
lf.show_merge_revno(rev, merge_depth, revno,
326
rev_tag_dict.get(rev_id))
328
lf.show_merge_revno(rev, merge_depth, revno)
331
if log_count >= limit:
335
def _get_mainline_revs(branch, start_revision, end_revision):
336
"""Get the mainline revisions from the branch.
338
Generates the list of mainline revisions for the branch.
340
:param branch: The branch containing the revisions.
342
:param start_revision: The first revision to be logged.
343
For backwards compatibility this may be a mainline integer revno,
344
but for merge revision support a RevisionInfo is expected.
346
:param end_revision: The last revision to be logged.
347
For backwards compatibility this may be a mainline integer revno,
348
but for merge revision support a RevisionInfo is expected.
350
:return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
352
which_revs = _enumerate_history(branch)
354
return None, None, None, None
356
# For mainline generation, map start_revision and end_revision to
357
# mainline revnos. If the revision is not on the mainline choose the
358
# appropriate extreme of the mainline instead - the extra will be
360
# Also map the revisions to rev_ids, to be used in the later filtering
363
if start_revision is None:
366
if isinstance(start_revision,RevisionInfo):
367
start_rev_id = start_revision.rev_id
368
start_revno = start_revision.revno or 1
370
branch.check_real_revno(start_revision)
371
start_revno = start_revision
374
if end_revision is None:
375
end_revno = len(which_revs)
377
if isinstance(end_revision,RevisionInfo):
378
end_rev_id = end_revision.rev_id
379
end_revno = end_revision.revno or len(which_revs)
381
branch.check_real_revno(end_revision)
382
end_revno = end_revision
384
if ((start_rev_id == NULL_REVISION)
385
or (end_rev_id == NULL_REVISION)):
386
raise BzrCommandError('Logging revision 0 is invalid.')
387
if start_revno > end_revno:
388
raise BzrCommandError("Start revision must be older than "
391
# list indexes are 0-based; revisions are 1-based
392
cut_revs = which_revs[(start_revno-1):(end_revno)]
394
return None, None, None, None
396
# convert the revision history to a dictionary:
397
rev_nos = dict((k, v) for v, k in cut_revs)
399
# override the mainline to look like the revision history.
400
mainline_revs = [revision_id for index, revision_id in cut_revs]
401
if cut_revs[0][0] == 1:
402
mainline_revs.insert(0, None)
404
mainline_revs.insert(0, which_revs[start_revno-2][1])
405
return mainline_revs, rev_nos, start_rev_id, end_rev_id
408
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
409
"""Filter view_revisions based on revision ranges.
411
:param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
412
tuples to be filtered.
414
:param start_rev_id: If not NONE specifies the first revision to be logged.
415
If NONE then all revisions up to the end_rev_id are logged.
417
:param end_rev_id: If not NONE specifies the last revision to be logged.
418
If NONE then all revisions up to the end of the log are logged.
420
:return: The filtered view_revisions.
422
if start_rev_id or end_rev_id:
423
revision_ids = [r for r, n, d in view_revisions]
425
start_index = revision_ids.index(start_rev_id)
428
if start_rev_id == end_rev_id:
429
end_index = start_index
432
end_index = revision_ids.index(end_rev_id)
434
end_index = len(view_revisions) - 1
435
# To include the revisions merged into the last revision,
436
# extend end_rev_id down to, but not including, the next rev
437
# with the same or lesser merge_depth
438
end_merge_depth = view_revisions[end_index][2]
440
for index in xrange(end_index+1, len(view_revisions)+1):
441
if view_revisions[index][2] <= end_merge_depth:
442
end_index = index - 1
445
# if the search falls off the end then log to the end as well
446
end_index = len(view_revisions) - 1
447
view_revisions = view_revisions[start_index:end_index+1]
448
return view_revisions
451
def _filter_revisions_touching_file_id(branch, file_id, mainline_revisions,
453
"""Return the list of revision ids which touch a given file id.
455
The function filters view_revisions and returns a subset.
456
This includes the revisions which directly change the file id,
457
and the revisions which merge these changes. So if the
465
And 'C' changes a file, then both C and D will be returned.
467
This will also can be restricted based on a subset of the mainline.
469
:return: A list of (revision_id, dotted_revno, merge_depth) tuples.
471
# find all the revisions that change the specific file
472
file_weave = branch.repository.weave_store.get_weave(file_id,
473
branch.repository.get_transaction())
474
weave_modifed_revisions = set(file_weave.versions())
475
# build the ancestry of each revision in the graph
476
# - only listing the ancestors that change the specific file.
477
rev_graph = branch.repository.get_revision_graph(mainline_revisions[-1])
478
sorted_rev_list = topo_sort(rev_graph)
480
for rev in sorted_rev_list:
481
parents = rev_graph[rev]
482
if rev not in weave_modifed_revisions and len(parents) == 1:
483
# We will not be adding anything new, so just use a reference to
484
# the parent ancestry.
485
rev_ancestry = ancestry[parents[0]]
488
if rev in weave_modifed_revisions:
489
rev_ancestry.add(rev)
490
for parent in parents:
491
rev_ancestry = rev_ancestry.union(ancestry[parent])
492
ancestry[rev] = rev_ancestry
494
def is_merging_rev(r):
495
parents = rev_graph[r]
497
leftparent = parents[0]
498
for rightparent in parents[1:]:
499
if not ancestry[leftparent].issuperset(
500
ancestry[rightparent]):
504
# filter from the view the revisions that did not change or merge
506
return [(r, n, d) for r, n, d in view_revs_iter
507
if r in weave_modifed_revisions or is_merging_rev(r)]
244
# a mainline revision.
247
if not delta.touches_file_id(specific_fileid):
251
# although we calculated it, throw it away without display
254
lf.show(revno, rev, delta)
256
lf.show_merge(rev, merge_depth)
510
259
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
563
class LogRevision(object):
564
"""A revision to be logged (by LogFormatter.log_revision).
566
A simple wrapper for the attributes of a revision to be logged.
567
The attributes may or may not be populated, as determined by the
568
logging options and the log formatter capabilities.
571
def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
575
self.merge_depth = merge_depth
580
313
class LogFormatter(object):
581
"""Abstract class to display log messages.
583
At a minimum, a derived class must implement the log_revision method.
585
If the LogFormatter needs to be informed of the beginning or end of
586
a log it should implement the begin_log and/or end_log hook methods.
588
A LogFormatter should define the following supports_XXX flags
589
to indicate which LogRevision attributes it supports:
591
- supports_delta must be True if this log formatter supports delta.
592
Otherwise the delta attribute may not be populated.
593
- supports_merge_revisions must be True if this log formatter supports
594
merge revisions. If not, and if supports_single_merge_revisions is
595
also not True, then only mainline revisions will be passed to the
597
- supports_single_merge_revision must be True if this log formatter
598
supports logging only a single merge revision. This flag is
599
only relevant if supports_merge_revisions is not True.
600
- supports_tags must be True if this log formatter supports tags.
601
Otherwise the tags attribute may not be populated.
314
"""Abstract class to display log messages."""
604
316
def __init__(self, to_file, show_ids=False, show_timezone='original'):
605
317
self.to_file = to_file
606
318
self.show_ids = show_ids
607
319
self.show_timezone = show_timezone
609
# TODO: uncomment this block after show() has been removed.
610
# Until then defining log_revision would prevent _show_log calling show()
611
# in legacy formatters.
612
# def log_revision(self, revision):
615
# :param revision: The LogRevision to be logged.
617
# raise NotImplementedError('not implemented in abstract base')
619
@deprecated_method(zero_seventeen)
620
321
def show(self, revno, rev, delta):
621
322
raise NotImplementedError('not implemented in abstract base')
623
324
def short_committer(self, rev):
624
name, address = config.parse_username(rev.committer)
629
def short_author(self, rev):
630
name, address = config.parse_username(rev.get_apparent_author())
325
return re.sub('<.*@.*>', '', rev.committer).strip(' ')
636
328
class LongLogFormatter(LogFormatter):
638
supports_merge_revisions = True
639
supports_delta = True
642
@deprecated_method(zero_seventeen)
643
def show(self, revno, rev, delta, tags=None):
644
lr = LogRevision(rev, revno, 0, delta, tags)
645
return self.log_revision(lr)
647
@deprecated_method(zero_seventeen)
648
def show_merge_revno(self, rev, merge_depth, revno, tags=None):
649
"""Show a merged revision rev, with merge_depth and a revno."""
650
lr = LogRevision(rev, revno, merge_depth, tags=tags)
651
return self.log_revision(lr)
653
def log_revision(self, revision):
654
"""Log a revision, either merged or not."""
655
indent = ' ' * revision.merge_depth
329
def show(self, revno, rev, delta):
330
return self._show_helper(revno=revno, rev=rev, delta=delta)
332
def show_merge(self, rev, merge_depth):
333
return self._show_helper(rev=rev, indent=' '*merge_depth, merged=True, delta=None)
335
def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
336
"""Show a revision, either merged or not."""
337
from bzrlib.osutils import format_date
656
338
to_file = self.to_file
657
to_file.write(indent + '-' * 60 + '\n')
658
if revision.revno is not None:
659
to_file.write(indent + 'revno: %s\n' % (revision.revno,))
661
to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
339
print >>to_file, indent+'-' * 60
340
if revno is not None:
341
print >>to_file, 'revno:', revno
343
print >>to_file, indent+'merged:', rev.revision_id
345
print >>to_file, indent+'revision-id:', rev.revision_id
662
346
if self.show_ids:
663
to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
665
for parent_id in revision.rev.parent_ids:
666
to_file.write(indent + 'parent: %s\n' % (parent_id,))
668
author = revision.rev.properties.get('author', None)
669
if author is not None:
670
to_file.write(indent + 'author: %s\n' % (author,))
671
to_file.write(indent + 'committer: %s\n' % (revision.rev.committer,))
673
branch_nick = revision.rev.properties.get('branch-nick', None)
674
if branch_nick is not None:
675
to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
677
date_str = format_date(revision.rev.timestamp,
678
revision.rev.timezone or 0,
347
for parent_id in rev.parent_ids:
348
print >>to_file, indent+'parent:', parent_id
349
print >>to_file, indent+'committer:', rev.committer
351
print >>to_file, indent+'branch nick: %s' % \
352
rev.properties['branch-nick']
355
date_str = format_date(rev.timestamp,
679
357
self.show_timezone)
680
to_file.write(indent + 'timestamp: %s\n' % (date_str,))
358
print >>to_file, indent+'timestamp: %s' % date_str
682
to_file.write(indent + 'message:\n')
683
if not revision.rev.message:
684
to_file.write(indent + ' (no message)\n')
360
print >>to_file, indent+'message:'
362
print >>to_file, indent+' (no message)'
686
message = revision.rev.message.rstrip('\r\n')
364
message = rev.message.rstrip('\r\n')
687
365
for l in message.split('\n'):
688
to_file.write(indent + ' %s\n' % (l,))
689
if revision.delta is not None:
690
revision.delta.show(to_file, self.show_ids, indent=indent)
366
print >>to_file, indent+' ' + l
367
if delta is not None:
368
delta.show(to_file, self.show_ids)
693
371
class ShortLogFormatter(LogFormatter):
695
supports_delta = True
696
supports_single_merge_revision = True
698
@deprecated_method(zero_seventeen)
699
372
def show(self, revno, rev, delta):
700
lr = LogRevision(rev, revno, 0, delta)
701
return self.log_revision(lr)
373
from bzrlib.osutils import format_date
703
def log_revision(self, revision):
704
375
to_file = self.to_file
705
date_str = format_date(revision.rev.timestamp,
706
revision.rev.timezone or 0,
709
if len(revision.rev.parent_ids) > 1:
710
is_merge = ' [merge]'
711
to_file.write("%5s %s\t%s%s\n" % (revision.revno,
712
self.short_author(revision.rev),
713
format_date(revision.rev.timestamp,
714
revision.rev.timezone or 0,
376
date_str = format_date(rev.timestamp, rev.timezone or 0,
378
print >>to_file, "%5d %s\t%s" % (revno, self.short_committer(rev),
379
format_date(rev.timestamp, rev.timezone or 0,
715
380
self.show_timezone, date_fmt="%Y-%m-%d",
718
382
if self.show_ids:
719
to_file.write(' revision-id:%s\n' % (revision.rev.revision_id,))
720
if not revision.rev.message:
721
to_file.write(' (no message)\n')
383
print >>to_file, ' revision-id:', rev.revision_id
385
print >>to_file, ' (no message)'
723
message = revision.rev.message.rstrip('\r\n')
387
message = rev.message.rstrip('\r\n')
724
388
for l in message.split('\n'):
725
to_file.write(' %s\n' % (l,))
389
print >>to_file, ' ' + l
727
391
# TODO: Why not show the modified files in a shorter form as
728
392
# well? rewrap them single lines of appropriate length
729
if revision.delta is not None:
730
revision.delta.show(to_file, self.show_ids)
393
if delta is not None:
394
delta.show(to_file, self.show_ids)
734
398
class LineLogFormatter(LogFormatter):
736
supports_single_merge_revision = True
738
def __init__(self, *args, **kwargs):
739
super(LineLogFormatter, self).__init__(*args, **kwargs)
740
self._max_chars = terminal_width() - 1
742
399
def truncate(self, str, max_len):
743
400
if len(str) <= max_len:
745
402
return str[:max_len-3]+'...'
747
404
def date_string(self, rev):
405
from bzrlib.osutils import format_date
748
406
return format_date(rev.timestamp, rev.timezone or 0,
749
407
self.show_timezone, date_fmt="%Y-%m-%d",
750
408
show_offset=False)