180
167
:param limit: If set, shows only 'limit' revisions, all revisions are shown
183
:param show_diff: If True, output a diff after each revision.
185
# Convert old-style parameters to new-style parameters
186
if specific_fileid is not None:
187
file_ids = [specific_fileid]
192
delta_type = 'partial'
199
diff_type = 'partial'
205
# Build the request and execute it
206
rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
207
start_revision=start_revision, end_revision=end_revision,
208
limit=limit, message_search=search,
209
delta_type=delta_type, diff_type=diff_type)
210
Logger(branch, rqst).show(lf)
213
# Note: This needs to be kept this in sync with the defaults in
214
# make_log_request_dict() below
215
_DEFAULT_REQUEST_PARAMS = {
216
'direction': 'reverse',
218
'generate_tags': True,
219
'_match_using_deltas': True,
223
def make_log_request_dict(direction='reverse', specific_fileids=None,
224
start_revision=None, end_revision=None, limit=None,
225
message_search=None, levels=1, generate_tags=True, delta_type=None,
226
diff_type=None, _match_using_deltas=True):
227
"""Convenience function for making a logging request dictionary.
229
Using this function may make code slightly safer by ensuring
230
parameters have the correct names. It also provides a reference
231
point for documenting the supported parameters.
233
:param direction: 'reverse' (default) is latest to earliest;
234
'forward' is earliest to latest.
236
:param specific_fileids: If not None, only include revisions
237
affecting the specified files, rather than all revisions.
239
:param start_revision: If not None, only generate
240
revisions >= start_revision
242
:param end_revision: If not None, only generate
243
revisions <= end_revision
245
:param limit: If set, generate only 'limit' revisions, all revisions
246
are shown if None or 0.
248
:param message_search: If not None, only include revisions with
249
matching commit messages
251
:param levels: the number of levels of revisions to
252
generate; 1 for just the mainline; 0 for all levels.
254
:param generate_tags: If True, include tags for matched revisions.
256
:param delta_type: Either 'full', 'partial' or None.
257
'full' means generate the complete delta - adds/deletes/modifies/etc;
258
'partial' means filter the delta using specific_fileids;
259
None means do not generate any delta.
261
:param diff_type: Either 'full', 'partial' or None.
262
'full' means generate the complete diff - adds/deletes/modifies/etc;
263
'partial' means filter the diff using specific_fileids;
264
None means do not generate any diff.
266
:param _match_using_deltas: a private parameter controlling the
267
algorithm used for matching specific_fileids. This parameter
268
may be removed in the future so bzrlib client code should NOT
272
'direction': direction,
273
'specific_fileids': specific_fileids,
274
'start_revision': start_revision,
275
'end_revision': end_revision,
277
'message_search': message_search,
279
'generate_tags': generate_tags,
280
'delta_type': delta_type,
281
'diff_type': diff_type,
282
# Add 'private' attributes for features that may be deprecated
283
'_match_using_deltas': _match_using_deltas,
287
def _apply_log_request_defaults(rqst):
288
"""Apply default values to a request dictionary."""
289
result = _DEFAULT_REQUEST_PARAMS
295
class LogGenerator(object):
296
"""A generator of log revisions."""
298
def iter_log_revisions(self):
299
"""Iterate over LogRevision objects.
301
:return: An iterator yielding LogRevision objects.
303
raise NotImplementedError(self.iter_log_revisions)
306
class Logger(object):
307
"""An object the generates, formats and displays a log."""
309
def __init__(self, branch, rqst):
312
:param branch: the branch to log
313
:param rqst: A dictionary specifying the query parameters.
314
See make_log_request_dict() for supported values.
317
self.rqst = _apply_log_request_defaults(rqst)
322
:param lf: The LogFormatter object to send the output to.
324
if not isinstance(lf, LogFormatter):
325
warn("not a LogFormatter instance: %r" % lf)
327
self.branch.lock_read()
329
if getattr(lf, 'begin_log', None):
332
if getattr(lf, 'end_log', None):
337
def _show_body(self, lf):
338
"""Show the main log output.
340
Subclasses may wish to override this.
342
# Tweak the LogRequest based on what the LogFormatter can handle.
343
# (There's no point generating stuff if the formatter can't display it.)
345
rqst['levels'] = lf.get_levels()
346
if not getattr(lf, 'supports_tags', False):
347
rqst['generate_tags'] = False
348
if not getattr(lf, 'supports_delta', False):
349
rqst['delta_type'] = None
350
if not getattr(lf, 'supports_diff', False):
351
rqst['diff_type'] = None
353
# Find and print the interesting revisions
354
generator = self._generator_factory(self.branch, rqst)
355
for lr in generator.iter_log_revisions():
172
if getattr(lf, 'begin_log', None):
175
_show_log(branch, lf, specific_fileid, verbose, direction,
176
start_revision, end_revision, search, limit)
178
if getattr(lf, 'end_log', None):
184
def _show_log(branch,
186
specific_fileid=None,
193
"""Worker function for show_log - see show_log."""
194
if not isinstance(lf, LogFormatter):
195
warn("not a LogFormatter instance: %r" % lf)
198
trace.mutter('get log for file_id %r', specific_fileid)
199
generate_merge_revisions = getattr(lf, 'supports_merge_revisions', False)
200
allow_single_merge_revision = getattr(lf,
201
'supports_single_merge_revision', False)
202
view_revisions = calculate_view_revisions(branch, start_revision,
203
end_revision, direction,
205
generate_merge_revisions,
206
allow_single_merge_revision)
208
generate_tags = getattr(lf, 'supports_tags', False)
210
if branch.supports_tags():
211
rev_tag_dict = branch.tags.get_reverse_tag_dict()
213
generate_delta = verbose and getattr(lf, 'supports_delta', False)
215
# now we just print all the revisions
217
revision_iterator = make_log_rev_iterator(branch, view_revisions,
218
generate_delta, search)
219
for revs in revision_iterator:
220
for (rev_id, revno, merge_depth), rev, delta in revs:
221
lr = LogRevision(rev, revno, merge_depth, delta,
222
rev_tag_dict.get(rev_id))
356
223
lf.log_revision(lr)
359
def _generator_factory(self, branch, rqst):
360
"""Make the LogGenerator object to use.
362
Subclasses may wish to override this.
364
return _DefaultLogGenerator(branch, rqst)
367
class _StartNotLinearAncestor(Exception):
368
"""Raised when a start revision is not found walking left-hand history."""
371
class _DefaultLogGenerator(LogGenerator):
372
"""The default generator of log revisions."""
374
def __init__(self, branch, rqst):
377
if rqst.get('generate_tags') and branch.supports_tags():
378
self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
380
self.rev_tag_dict = {}
382
def iter_log_revisions(self):
383
"""Iterate over LogRevision objects.
385
:return: An iterator yielding LogRevision objects.
388
levels = rqst.get('levels')
389
limit = rqst.get('limit')
390
diff_type = rqst.get('diff_type')
392
revision_iterator = self._create_log_revision_iterator()
393
for revs in revision_iterator:
394
for (rev_id, revno, merge_depth), rev, delta in revs:
395
# 0 levels means show everything; merge_depth counts from 0
396
if levels != 0 and merge_depth >= levels:
398
if diff_type is None:
401
diff = self._format_diff(rev, rev_id, diff_type)
402
yield LogRevision(rev, revno, merge_depth, delta,
403
self.rev_tag_dict.get(rev_id), diff)
406
if log_count >= limit:
409
def _format_diff(self, rev, rev_id, diff_type):
410
repo = self.branch.repository
411
if len(rev.parent_ids) == 0:
412
ancestor_id = _mod_revision.NULL_REVISION
414
ancestor_id = rev.parent_ids[0]
415
tree_1 = repo.revision_tree(ancestor_id)
416
tree_2 = repo.revision_tree(rev_id)
417
file_ids = self.rqst.get('specific_fileids')
418
if diff_type == 'partial' and file_ids is not None:
419
specific_files = [tree_2.id2path(id) for id in file_ids]
421
specific_files = None
423
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
427
def _create_log_revision_iterator(self):
428
"""Create a revision iterator for log.
430
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
433
self.start_rev_id, self.end_rev_id = _get_revision_limits(
434
self.branch, self.rqst.get('start_revision'),
435
self.rqst.get('end_revision'))
436
if self.rqst.get('_match_using_deltas'):
437
return self._log_revision_iterator_using_delta_matching()
439
# We're using the per-file-graph algorithm. This scales really
440
# well but only makes sense if there is a single file and it's
442
file_count = len(self.rqst.get('specific_fileids'))
444
raise BzrError("illegal LogRequest: must match-using-deltas "
445
"when logging %d files" % file_count)
446
return self._log_revision_iterator_using_per_file_graph()
448
def _log_revision_iterator_using_delta_matching(self):
449
# Get the base revisions, filtering by the revision range
451
generate_merge_revisions = rqst.get('levels') != 1
452
delayed_graph_generation = not rqst.get('specific_fileids') and (
453
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
454
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
455
self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
456
delayed_graph_generation=delayed_graph_generation)
458
# Apply the other filters
459
return make_log_rev_iterator(self.branch, view_revisions,
460
rqst.get('delta_type'), rqst.get('message_search'),
461
file_ids=rqst.get('specific_fileids'),
462
direction=rqst.get('direction'))
464
def _log_revision_iterator_using_per_file_graph(self):
465
# Get the base revisions, filtering by the revision range.
466
# Note that we always generate the merge revisions because
467
# filter_revisions_touching_file_id() requires them ...
469
view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
470
self.end_rev_id, rqst.get('direction'), True)
471
if not isinstance(view_revisions, list):
472
view_revisions = list(view_revisions)
473
view_revisions = _filter_revisions_touching_file_id(self.branch,
474
rqst.get('specific_fileids')[0], view_revisions,
475
include_merges=rqst.get('levels') != 1)
476
return make_log_rev_iterator(self.branch, view_revisions,
477
rqst.get('delta_type'), rqst.get('message_search'))
480
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
481
generate_merge_revisions, delayed_graph_generation=False):
482
"""Calculate the revisions to view.
484
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
485
a list of the same tuples.
487
br_revno, br_rev_id = branch.last_revision_info()
226
if log_count >= limit:
230
def calculate_view_revisions(branch, start_revision, end_revision, direction,
231
specific_fileid, generate_merge_revisions,
232
allow_single_merge_revision):
233
if ( not generate_merge_revisions
234
and start_revision is end_revision is None
235
and direction == 'reverse'
236
and specific_fileid is None):
237
return _linear_view_revisions(branch)
239
mainline_revs, rev_nos, start_rev_id, end_rev_id = _get_mainline_revs(
240
branch, start_revision, end_revision)
241
if not mainline_revs:
491
# If a single revision is requested, check we can handle it
492
generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
493
(not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
494
if generate_single_revision:
495
return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
497
# If we only want to see linear revisions, we can iterate ...
498
if not generate_merge_revisions:
499
return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
502
return _generate_all_revisions(branch, start_rev_id, end_rev_id,
503
direction, delayed_graph_generation)
506
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
507
if rev_id == br_rev_id:
509
return [(br_rev_id, br_revno, 0)]
511
revno = branch.revision_id_to_dotted_revno(rev_id)
512
revno_str = '.'.join(str(n) for n in revno)
513
return [(rev_id, revno_str, 0)]
516
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
517
result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
518
# If a start limit was given and it's not obviously an
519
# ancestor of the end limit, check it before outputting anything
520
if direction == 'forward' or (start_rev_id
521
and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
523
result = list(result)
524
except _StartNotLinearAncestor:
525
raise errors.BzrCommandError('Start revision not found in'
526
' left-hand history of end revision.')
527
if direction == 'forward':
528
result = reversed(result)
532
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
533
delayed_graph_generation):
534
# On large trees, generating the merge graph can take 30-60 seconds
535
# so we delay doing it until a merge is detected, incrementally
536
# returning initial (non-merge) revisions while we can.
537
initial_revisions = []
538
if delayed_graph_generation:
540
for rev_id, revno, depth in \
541
_linear_view_revisions(branch, start_rev_id, end_rev_id):
542
if _has_merges(branch, rev_id):
546
initial_revisions.append((rev_id, revno, depth))
548
# No merged revisions found
549
if direction == 'reverse':
550
return initial_revisions
551
elif direction == 'forward':
552
return reversed(initial_revisions)
554
raise ValueError('invalid direction %r' % direction)
555
except _StartNotLinearAncestor:
556
# A merge was never detected so the lower revision limit can't
557
# be nested down somewhere
558
raise errors.BzrCommandError('Start revision not found in'
559
' history of end revision.')
561
# A log including nested merges is required. If the direction is reverse,
562
# we rebase the initial merge depths so that the development line is
563
# shown naturally, i.e. just like it is for linear logging. We can easily
564
# make forward the exact opposite display, but showing the merge revisions
565
# indented at the end seems slightly nicer in that case.
566
view_revisions = chain(iter(initial_revisions),
567
_graph_view_revisions(branch, start_rev_id, end_rev_id,
568
rebase_initial_depths=direction == 'reverse'))
244
generate_single_revision = False
245
if ((not generate_merge_revisions)
246
and ((start_rev_id and (start_rev_id not in rev_nos))
247
or (end_rev_id and (end_rev_id not in rev_nos)))):
248
generate_single_revision = ((start_rev_id == end_rev_id)
249
and allow_single_merge_revision)
250
if not generate_single_revision:
251
raise errors.BzrCommandError('Selected log formatter only supports'
252
' mainline revisions.')
253
generate_merge_revisions = generate_single_revision
254
view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
255
direction, include_merges=generate_merge_revisions)
569
257
if direction == 'reverse':
570
return view_revisions
571
elif direction == 'forward':
572
# Forward means oldest first, adjusting for depth.
573
view_revisions = reverse_by_depth(list(view_revisions))
574
return _rebase_merge_depth(view_revisions)
576
raise ValueError('invalid direction %r' % direction)
579
def _has_merges(branch, rev_id):
580
"""Does a revision have multiple parents or not?"""
581
parents = branch.repository.get_parent_map([rev_id]).get(rev_id, [])
582
return len(parents) > 1
585
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
586
"""Is start_rev_id an obvious ancestor of end_rev_id?"""
587
if start_rev_id and end_rev_id:
588
start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
589
end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
590
if len(start_dotted) == 1 and len(end_dotted) == 1:
592
return start_dotted[0] <= end_dotted[0]
593
elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
594
start_dotted[0:1] == end_dotted[0:1]):
595
# both on same development line
596
return start_dotted[2] <= end_dotted[2]
603
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
604
"""Calculate a sequence of revisions to view, newest to oldest.
606
:param start_rev_id: the lower revision-id
607
:param end_rev_id: the upper revision-id
608
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
609
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
610
is not found walking the left-hand history
612
br_revno, br_rev_id = branch.last_revision_info()
613
repo = branch.repository
614
if start_rev_id is None and end_rev_id is None:
616
for revision_id in repo.iter_reverse_revision_history(br_rev_id):
617
yield revision_id, str(cur_revno), 0
620
if end_rev_id is None:
621
end_rev_id = br_rev_id
622
found_start = start_rev_id is None
623
for revision_id in repo.iter_reverse_revision_history(end_rev_id):
624
revno = branch.revision_id_to_dotted_revno(revision_id)
625
revno_str = '.'.join(str(n) for n in revno)
626
if not found_start and revision_id == start_rev_id:
627
yield revision_id, revno_str, 0
631
yield revision_id, revno_str, 0
634
raise _StartNotLinearAncestor()
637
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
638
rebase_initial_depths=True):
639
"""Calculate revisions to view including merges, newest to oldest.
641
:param branch: the branch
642
:param start_rev_id: the lower revision-id
643
:param end_rev_id: the upper revision-id
644
:param rebase_initial_depth: should depths be rebased until a mainline
646
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
648
view_revisions = branch.iter_merge_sorted_revisions(
649
start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
650
stop_rule="with-merges")
651
if not rebase_initial_depths:
652
for (rev_id, merge_depth, revno, end_of_merge
654
yield rev_id, '.'.join(map(str, revno)), merge_depth
656
# We're following a development line starting at a merged revision.
657
# We need to adjust depths down by the initial depth until we find
658
# a depth less than it. Then we use that depth as the adjustment.
659
# If and when we reach the mainline, depth adjustment ends.
660
depth_adjustment = None
661
for (rev_id, merge_depth, revno, end_of_merge
663
if depth_adjustment is None:
664
depth_adjustment = merge_depth
666
if merge_depth < depth_adjustment:
667
depth_adjustment = merge_depth
668
merge_depth -= depth_adjustment
669
yield rev_id, '.'.join(map(str, revno)), merge_depth
672
def calculate_view_revisions(branch, start_revision, end_revision, direction,
673
specific_fileid, generate_merge_revisions):
674
"""Calculate the revisions to view.
676
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
677
a list of the same tuples.
679
# This method is no longer called by the main code path.
680
# It is retained for API compatibility and may be deprecated
682
start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
684
view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
685
direction, generate_merge_revisions or specific_fileid))
258
start_rev_id, end_rev_id = end_rev_id, start_rev_id
259
view_revisions = _filter_revision_range(list(view_revs_iter),
262
if view_revisions and generate_single_revision:
263
view_revisions = view_revisions[0:1]
686
264
if specific_fileid:
687
265
view_revisions = _filter_revisions_touching_file_id(branch,
688
specific_fileid, view_revisions,
689
include_merges=generate_merge_revisions)
690
return _rebase_merge_depth(view_revisions)
693
def _rebase_merge_depth(view_revisions):
694
"""Adjust depths upwards so the top level is 0."""
695
# If either the first or last revision have a merge_depth of 0, we're done
269
# rebase merge_depth - unless there are no revisions or
270
# either the first or last revision have merge_depth = 0.
696
271
if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
697
272
min_depth = min([d for r,n,d in view_revisions])
698
273
if min_depth != 0:
1367
760
def short_author(self, rev):
1368
name, address = config.parse_username(rev.get_apparent_authors()[0])
761
name, address = config.parse_username(rev.get_apparent_author())
1373
def merge_marker(self, revision):
1374
"""Get the merge marker to include in the output or '' if none."""
1375
if len(revision.rev.parent_ids) > 1:
1376
self._merge_count += 1
1381
766
def show_properties(self, revision, indent):
1382
767
"""Displays the custom properties returned by each registered handler.
1384
If a registered handler raises an error it is propagated.
1386
for line in self.custom_properties(revision):
1387
self.to_file.write("%s%s\n" % (indent, line))
1389
def custom_properties(self, revision):
1390
"""Format the custom properties returned by each registered handler.
1392
If a registered handler raises an error it is propagated.
1394
:return: a list of formatted lines (excluding trailing newlines)
1396
lines = self._foreign_info_properties(revision)
769
If a registered handler raises an error it is propagated.
1397
771
for key, handler in properties_handler_registry.iteritems():
1398
lines.extend(self._format_properties(handler(revision)))
1401
def _foreign_info_properties(self, rev):
1402
"""Custom log displayer for foreign revision identifiers.
1404
:param rev: Revision object.
1406
# Revision comes directly from a foreign repository
1407
if isinstance(rev, foreign.ForeignRevision):
1408
return rev.mapping.vcs.show_foreign_revid(rev.foreign_revid)
1410
# Imported foreign revision revision ids always contain :
1411
if not ":" in rev.revision_id:
1414
# Revision was once imported from a foreign repository
1416
foreign_revid, mapping = \
1417
foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1418
except errors.InvalidRevisionId:
1421
return self._format_properties(
1422
mapping.vcs.show_foreign_revid(foreign_revid))
1424
def _format_properties(self, properties):
1426
for key, value in properties.items():
1427
lines.append(key + ': ' + value)
1430
def show_diff(self, to_file, diff, indent):
1431
for l in diff.rstrip().split('\n'):
1432
to_file.write(indent + '%s\n' % (l,))
1435
# Separator between revisions in long format
1436
_LONG_SEP = '-' * 60
772
for key, value in handler(revision).items():
773
self.to_file.write(indent + key + ': ' + value + '\n')
1439
776
class LongLogFormatter(LogFormatter):
1441
778
supports_merge_revisions = True
1442
preferred_levels = 1
1443
779
supports_delta = True
1444
780
supports_tags = True
1445
supports_diff = True
1447
def __init__(self, *args, **kwargs):
1448
super(LongLogFormatter, self).__init__(*args, **kwargs)
1449
if self.show_timezone == 'original':
1450
self.date_string = self._date_string_original_timezone
1452
self.date_string = self._date_string_with_timezone
1454
def _date_string_with_timezone(self, rev):
1455
return format_date(rev.timestamp, rev.timezone or 0,
1458
def _date_string_original_timezone(self, rev):
1459
return format_date_with_offset_in_original_timezone(rev.timestamp,
1462
782
def log_revision(self, revision):
1463
783
"""Log a revision, either merged or not."""
1464
784
indent = ' ' * revision.merge_depth
785
to_file = self.to_file
786
to_file.write(indent + '-' * 60 + '\n')
1466
787
if revision.revno is not None:
1467
lines.append('revno: %s%s' % (revision.revno,
1468
self.merge_marker(revision)))
788
to_file.write(indent + 'revno: %s\n' % (revision.revno,))
1469
789
if revision.tags:
1470
lines.append('tags: %s' % (', '.join(revision.tags)))
790
to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
1471
791
if self.show_ids:
1472
lines.append('revision-id: %s' % (revision.rev.revision_id,))
792
to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1473
794
for parent_id in revision.rev.parent_ids:
1474
lines.append('parent: %s' % (parent_id,))
1475
lines.extend(self.custom_properties(revision.rev))
795
to_file.write(indent + 'parent: %s\n' % (parent_id,))
796
self.show_properties(revision.rev, indent)
1477
committer = revision.rev.committer
1478
authors = revision.rev.get_apparent_authors()
1479
if authors != [committer]:
1480
lines.append('author: %s' % (", ".join(authors),))
1481
lines.append('committer: %s' % (committer,))
798
author = revision.rev.properties.get('author', None)
799
if author is not None:
800
to_file.write(indent + 'author: %s\n' % (author,))
801
to_file.write(indent + 'committer: %s\n' % (revision.rev.committer,))
1483
803
branch_nick = revision.rev.properties.get('branch-nick', None)
1484
804
if branch_nick is not None:
1485
lines.append('branch nick: %s' % (branch_nick,))
1487
lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1489
lines.append('message:')
805
to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
807
date_str = format_date(revision.rev.timestamp,
808
revision.rev.timezone or 0,
810
to_file.write(indent + 'timestamp: %s\n' % (date_str,))
812
to_file.write(indent + 'message:\n')
1490
813
if not revision.rev.message:
1491
lines.append(' (no message)')
814
to_file.write(indent + ' (no message)\n')
1493
816
message = revision.rev.message.rstrip('\r\n')
1494
817
for l in message.split('\n'):
1495
lines.append(' %s' % (l,))
1497
# Dump the output, appending the delta and diff if requested
1498
to_file = self.to_file
1499
to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
818
to_file.write(indent + ' %s\n' % (l,))
1500
819
if revision.delta is not None:
1501
820
# We don't respect delta_format for compatibility
1502
821
revision.delta.show(to_file, self.show_ids, indent=indent,
1503
822
short_status=False)
1504
if revision.diff is not None:
1505
to_file.write(indent + 'diff:\n')
1507
# Note: we explicitly don't indent the diff (relative to the
1508
# revision information) so that the output can be fed to patch -p0
1509
self.show_diff(self.to_exact_file, revision.diff, indent)
1510
self.to_exact_file.flush()
1512
def get_advice_separator(self):
1513
"""Get the text separating the log from the closing advice."""
1514
return '-' * 60 + '\n'
1517
825
class ShortLogFormatter(LogFormatter):
1519
supports_merge_revisions = True
1520
preferred_levels = 1
1521
827
supports_delta = True
1522
supports_tags = True
1523
supports_diff = True
1525
def __init__(self, *args, **kwargs):
1526
super(ShortLogFormatter, self).__init__(*args, **kwargs)
1527
self.revno_width_by_depth = {}
828
supports_single_merge_revision = True
1529
830
def log_revision(self, revision):
1530
# We need two indents: one per depth and one for the information
1531
# relative to that indent. Most mainline revnos are 5 chars or
1532
# less while dotted revnos are typically 11 chars or less. Once
1533
# calculated, we need to remember the offset for a given depth
1534
# as we might be starting from a dotted revno in the first column
1535
# and we want subsequent mainline revisions to line up.
1536
depth = revision.merge_depth
1537
indent = ' ' * depth
1538
revno_width = self.revno_width_by_depth.get(depth)
1539
if revno_width is None:
1540
if revision.revno.find('.') == -1:
1541
# mainline revno, e.g. 12345
1544
# dotted revno, e.g. 12345.10.55
1546
self.revno_width_by_depth[depth] = revno_width
1547
offset = ' ' * (revno_width + 1)
1549
831
to_file = self.to_file
1552
tags = ' {%s}' % (', '.join(revision.tags))
1553
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1554
revision.revno, self.short_author(revision.rev),
833
if len(revision.rev.parent_ids) > 1:
834
is_merge = ' [merge]'
835
to_file.write("%5s %s\t%s%s\n" % (revision.revno,
836
self.short_author(revision.rev),
1555
837
format_date(revision.rev.timestamp,
1556
838
revision.rev.timezone or 0,
1557
839
self.show_timezone, date_fmt="%Y-%m-%d",
1558
840
show_offset=False),
1559
tags, self.merge_marker(revision)))
1560
self.show_properties(revision.rev, indent+offset)
1561
842
if self.show_ids:
1562
to_file.write(indent + offset + 'revision-id:%s\n'
843
to_file.write(' revision-id:%s\n'
1563
844
% (revision.rev.revision_id,))
1564
845
if not revision.rev.message:
1565
to_file.write(indent + offset + '(no message)\n')
846
to_file.write(' (no message)\n')
1567
848
message = revision.rev.message.rstrip('\r\n')
1568
849
for l in message.split('\n'):
1569
to_file.write(indent + offset + '%s\n' % (l,))
850
to_file.write(' %s\n' % (l,))
1571
852
if revision.delta is not None:
1572
revision.delta.show(to_file, self.show_ids, indent=indent + offset,
853
revision.delta.show(to_file, self.show_ids,
1573
854
short_status=self.delta_format==1)
1574
if revision.diff is not None:
1575
self.show_diff(self.to_exact_file, revision.diff, ' ')
1576
855
to_file.write('\n')
1579
858
class LineLogFormatter(LogFormatter):
1581
supports_merge_revisions = True
1582
preferred_levels = 1
1583
supports_tags = True
860
supports_single_merge_revision = True
1585
862
def __init__(self, *args, **kwargs):
1586
863
super(LineLogFormatter, self).__init__(*args, **kwargs)
1587
width = terminal_width()
1588
if width is not None:
1589
# we need one extra space for terminals that wrap on last char
1591
self._max_chars = width
864
self._max_chars = terminal_width() - 1
1593
866
def truncate(self, str, max_len):
1594
if max_len is None or len(str) <= max_len:
867
if len(str) <= max_len:
1596
return str[:max_len-3] + '...'
869
return str[:max_len-3]+'...'
1598
871
def date_string(self, rev):
1599
872
return format_date(rev.timestamp, rev.timezone or 0,