~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2009 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
 
18
 
 
19
17
"""Code to show logs of changes.
20
18
 
21
19
Various flavors of log can be produced:
49
47
all the changes since the previous revision that touched hello.c.
50
48
"""
51
49
 
 
50
from __future__ import absolute_import
 
51
 
52
52
import codecs
53
53
from cStringIO import StringIO
54
54
from itertools import (
65
65
lazy_import(globals(), """
66
66
 
67
67
from bzrlib import (
68
 
    bzrdir,
69
68
    config,
 
69
    controldir,
70
70
    diff,
71
71
    errors,
72
72
    foreign,
73
73
    repository as _mod_repository,
74
74
    revision as _mod_revision,
75
75
    revisionspec,
76
 
    trace,
77
76
    tsort,
78
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
79
79
""")
80
80
 
81
81
from bzrlib import (
 
82
    lazy_regex,
82
83
    registry,
83
84
    )
84
85
from bzrlib.osutils import (
85
86
    format_date,
86
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
87
89
    get_terminal_encoding,
88
 
    re_compile_checked,
89
90
    terminal_width,
90
91
    )
91
92
 
104
105
    last_ie = None
105
106
    last_path = None
106
107
    revno = 1
107
 
    for revision_id in branch.revision_history():
108
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
109
 
        if file_id in this_inv:
 
108
    graph = branch.repository.get_graph()
 
109
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
110
        [_mod_revision.NULL_REVISION]))
 
111
    for revision_id in reversed(history):
 
112
        this_inv = branch.repository.get_inventory(revision_id)
 
113
        if this_inv.has_id(file_id):
110
114
            this_ie = this_inv[file_id]
111
115
            this_path = this_inv.id2path(file_id)
112
116
        else:
134
138
        revno += 1
135
139
 
136
140
 
137
 
def _enumerate_history(branch):
138
 
    rh = []
139
 
    revno = 1
140
 
    for rev_id in branch.revision_history():
141
 
        rh.append((revno, rev_id))
142
 
        revno += 1
143
 
    return rh
144
 
 
145
 
 
146
141
def show_log(branch,
147
142
             lf,
148
143
             specific_fileid=None,
152
147
             end_revision=None,
153
148
             search=None,
154
149
             limit=None,
155
 
             show_diff=False):
 
150
             show_diff=False,
 
151
             match=None):
156
152
    """Write out human-readable log of commits to this branch.
157
153
 
158
154
    This function is being retained for backwards compatibility but
181
177
        if None or 0.
182
178
 
183
179
    :param show_diff: If True, output a diff after each revision.
 
180
 
 
181
    :param match: Dictionary of search lists to use when matching revision
 
182
      properties.
184
183
    """
185
184
    # Convert old-style parameters to new-style parameters
186
185
    if specific_fileid is not None:
210
209
    Logger(branch, rqst).show(lf)
211
210
 
212
211
 
213
 
# Note: This needs to be kept this in sync with the defaults in
 
212
# Note: This needs to be kept in sync with the defaults in
214
213
# make_log_request_dict() below
215
214
_DEFAULT_REQUEST_PARAMS = {
216
215
    'direction': 'reverse',
217
 
    'levels': 1,
 
216
    'levels': None,
218
217
    'generate_tags': True,
 
218
    'exclude_common_ancestry': False,
219
219
    '_match_using_deltas': True,
220
220
    }
221
221
 
222
222
 
223
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):
 
224
                          start_revision=None, end_revision=None, limit=None,
 
225
                          message_search=None, levels=None, generate_tags=True,
 
226
                          delta_type=None,
 
227
                          diff_type=None, _match_using_deltas=True,
 
228
                          exclude_common_ancestry=False, match=None,
 
229
                          signature=False, omit_merges=False,
 
230
                          ):
227
231
    """Convenience function for making a logging request dictionary.
228
232
 
229
233
    Using this function may make code slightly safer by ensuring
249
253
      matching commit messages
250
254
 
251
255
    :param levels: the number of levels of revisions to
252
 
      generate; 1 for just the mainline; 0 for all levels.
 
256
      generate; 1 for just the mainline; 0 for all levels, or None for
 
257
      a sensible default.
253
258
 
254
259
    :param generate_tags: If True, include tags for matched revisions.
255
 
 
 
260
`
256
261
    :param delta_type: Either 'full', 'partial' or None.
257
262
      'full' means generate the complete delta - adds/deletes/modifies/etc;
258
263
      'partial' means filter the delta using specific_fileids;
267
272
      algorithm used for matching specific_fileids. This parameter
268
273
      may be removed in the future so bzrlib client code should NOT
269
274
      use it.
 
275
 
 
276
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
 
277
      range operator or as a graph difference.
 
278
 
 
279
    :param signature: show digital signature information
 
280
 
 
281
    :param match: Dictionary of list of search strings to use when filtering
 
282
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
283
      the empty string to match any of the preceding properties.
 
284
 
 
285
    :param omit_merges: If True, commits with more than one parent are
 
286
      omitted.
 
287
 
270
288
    """
 
289
    # Take care of old style message_search parameter
 
290
    if message_search:
 
291
        if match:
 
292
            if 'message' in match:
 
293
                match['message'].append(message_search)
 
294
            else:
 
295
                match['message'] = [message_search]
 
296
        else:
 
297
            match={ 'message': [message_search] }
271
298
    return {
272
299
        'direction': direction,
273
300
        'specific_fileids': specific_fileids,
274
301
        'start_revision': start_revision,
275
302
        'end_revision': end_revision,
276
303
        'limit': limit,
277
 
        'message_search': message_search,
278
304
        'levels': levels,
279
305
        'generate_tags': generate_tags,
280
306
        'delta_type': delta_type,
281
307
        'diff_type': diff_type,
 
308
        'exclude_common_ancestry': exclude_common_ancestry,
 
309
        'signature': signature,
 
310
        'match': match,
 
311
        'omit_merges': omit_merges,
282
312
        # Add 'private' attributes for features that may be deprecated
283
313
        '_match_using_deltas': _match_using_deltas,
284
314
    }
286
316
 
287
317
def _apply_log_request_defaults(rqst):
288
318
    """Apply default values to a request dictionary."""
289
 
    result = _DEFAULT_REQUEST_PARAMS
 
319
    result = _DEFAULT_REQUEST_PARAMS.copy()
290
320
    if rqst:
291
321
        result.update(rqst)
292
322
    return result
293
323
 
294
324
 
 
325
def format_signature_validity(rev_id, repo):
 
326
    """get the signature validity
 
327
 
 
328
    :param rev_id: revision id to validate
 
329
    :param repo: repository of revision
 
330
    :return: human readable string to print to log
 
331
    """
 
332
    from bzrlib import gpg
 
333
 
 
334
    gpg_strategy = gpg.GPGStrategy(None)
 
335
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
 
336
    if result[0] == gpg.SIGNATURE_VALID:
 
337
        return u"valid signature from {0}".format(result[1])
 
338
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
339
        return "unknown key {0}".format(result[1])
 
340
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
341
        return "invalid signature!"
 
342
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
343
        return "no signature"
 
344
 
 
345
 
295
346
class LogGenerator(object):
296
347
    """A generator of log revisions."""
297
348
 
304
355
 
305
356
 
306
357
class Logger(object):
307
 
    """An object the generates, formats and displays a log."""
 
358
    """An object that generates, formats and displays a log."""
308
359
 
309
360
    def __init__(self, branch, rqst):
310
361
        """Create a Logger.
342
393
        # Tweak the LogRequest based on what the LogFormatter can handle.
343
394
        # (There's no point generating stuff if the formatter can't display it.)
344
395
        rqst = self.rqst
345
 
        rqst['levels'] = lf.get_levels()
 
396
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
397
            # user didn't specify levels, use whatever the LF can handle:
 
398
            rqst['levels'] = lf.get_levels()
 
399
 
346
400
        if not getattr(lf, 'supports_tags', False):
347
401
            rqst['generate_tags'] = False
348
402
        if not getattr(lf, 'supports_delta', False):
349
403
            rqst['delta_type'] = None
350
404
        if not getattr(lf, 'supports_diff', False):
351
405
            rqst['diff_type'] = None
 
406
        if not getattr(lf, 'supports_signatures', False):
 
407
            rqst['signature'] = False
352
408
 
353
409
        # Find and print the interesting revisions
354
410
        generator = self._generator_factory(self.branch, rqst)
358
414
 
359
415
    def _generator_factory(self, branch, rqst):
360
416
        """Make the LogGenerator object to use.
361
 
        
 
417
 
362
418
        Subclasses may wish to override this.
363
419
        """
364
420
        return _DefaultLogGenerator(branch, rqst)
388
444
        levels = rqst.get('levels')
389
445
        limit = rqst.get('limit')
390
446
        diff_type = rqst.get('diff_type')
 
447
        show_signature = rqst.get('signature')
 
448
        omit_merges = rqst.get('omit_merges')
391
449
        log_count = 0
392
450
        revision_iterator = self._create_log_revision_iterator()
393
451
        for revs in revision_iterator:
395
453
                # 0 levels means show everything; merge_depth counts from 0
396
454
                if levels != 0 and merge_depth >= levels:
397
455
                    continue
 
456
                if omit_merges and len(rev.parent_ids) > 1:
 
457
                    continue
398
458
                if diff_type is None:
399
459
                    diff = None
400
460
                else:
401
461
                    diff = self._format_diff(rev, rev_id, diff_type)
 
462
                if show_signature:
 
463
                    signature = format_signature_validity(rev_id,
 
464
                                                self.branch.repository)
 
465
                else:
 
466
                    signature = None
402
467
                yield LogRevision(rev, revno, merge_depth, delta,
403
 
                    self.rev_tag_dict.get(rev_id), diff)
 
468
                    self.rev_tag_dict.get(rev_id), diff, signature)
404
469
                if limit:
405
470
                    log_count += 1
406
471
                    if log_count >= limit:
420
485
        else:
421
486
            specific_files = None
422
487
        s = StringIO()
 
488
        path_encoding = get_diff_header_encoding()
423
489
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
424
 
            new_label='')
 
490
            new_label='', path_encoding=path_encoding)
425
491
        return s.getvalue()
426
492
 
427
493
    def _create_log_revision_iterator(self):
451
517
        generate_merge_revisions = rqst.get('levels') != 1
452
518
        delayed_graph_generation = not rqst.get('specific_fileids') and (
453
519
                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)
 
520
        view_revisions = _calc_view_revisions(
 
521
            self.branch, self.start_rev_id, self.end_rev_id,
 
522
            rqst.get('direction'),
 
523
            generate_merge_revisions=generate_merge_revisions,
 
524
            delayed_graph_generation=delayed_graph_generation,
 
525
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
457
526
 
458
527
        # Apply the other filters
459
528
        return make_log_rev_iterator(self.branch, view_revisions,
460
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
529
            rqst.get('delta_type'), rqst.get('match'),
461
530
            file_ids=rqst.get('specific_fileids'),
462
531
            direction=rqst.get('direction'))
463
532
 
466
535
        # Note that we always generate the merge revisions because
467
536
        # filter_revisions_touching_file_id() requires them ...
468
537
        rqst = self.rqst
469
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
470
 
            self.end_rev_id, rqst.get('direction'), True)
 
538
        view_revisions = _calc_view_revisions(
 
539
            self.branch, self.start_rev_id, self.end_rev_id,
 
540
            rqst.get('direction'), generate_merge_revisions=True,
 
541
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
471
542
        if not isinstance(view_revisions, list):
472
543
            view_revisions = list(view_revisions)
473
544
        view_revisions = _filter_revisions_touching_file_id(self.branch,
474
545
            rqst.get('specific_fileids')[0], view_revisions,
475
546
            include_merges=rqst.get('levels') != 1)
476
547
        return make_log_rev_iterator(self.branch, view_revisions,
477
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
548
            rqst.get('delta_type'), rqst.get('match'))
478
549
 
479
550
 
480
551
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
481
 
    generate_merge_revisions, delayed_graph_generation=False):
 
552
                         generate_merge_revisions,
 
553
                         delayed_graph_generation=False,
 
554
                         exclude_common_ancestry=False,
 
555
                         ):
482
556
    """Calculate the revisions to view.
483
557
 
484
558
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
485
559
             a list of the same tuples.
486
560
    """
 
561
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
 
562
        raise errors.BzrCommandError(gettext(
 
563
            '--exclude-common-ancestry requires two different revisions'))
 
564
    if direction not in ('reverse', 'forward'):
 
565
        raise ValueError(gettext('invalid direction %r') % direction)
487
566
    br_revno, br_rev_id = branch.last_revision_info()
488
567
    if br_revno == 0:
489
568
        return []
490
569
 
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)
496
 
 
497
 
    # If we only want to see linear revisions, we can iterate ...
 
570
    if (end_rev_id and start_rev_id == end_rev_id
 
571
        and (not generate_merge_revisions
 
572
             or not _has_merges(branch, end_rev_id))):
 
573
        # If a single revision is requested, check we can handle it
 
574
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
575
                                       br_revno)
498
576
    if not generate_merge_revisions:
499
 
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
500
 
            direction)
501
 
    else:
502
 
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
503
 
            direction, delayed_graph_generation)
 
577
        try:
 
578
            # If we only want to see linear revisions, we can iterate ...
 
579
            iter_revs = _linear_view_revisions(
 
580
                branch, start_rev_id, end_rev_id,
 
581
                exclude_common_ancestry=exclude_common_ancestry)
 
582
            # If a start limit was given and it's not obviously an
 
583
            # ancestor of the end limit, check it before outputting anything
 
584
            if (direction == 'forward'
 
585
                or (start_rev_id and not _is_obvious_ancestor(
 
586
                        branch, start_rev_id, end_rev_id))):
 
587
                    iter_revs = list(iter_revs)
 
588
            if direction == 'forward':
 
589
                iter_revs = reversed(iter_revs)
 
590
            return iter_revs
 
591
        except _StartNotLinearAncestor:
 
592
            # Switch to the slower implementation that may be able to find a
 
593
            # non-obvious ancestor out of the left-hand history.
 
594
            pass
 
595
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
596
                                        direction, delayed_graph_generation,
 
597
                                        exclude_common_ancestry)
 
598
    if direction == 'forward':
 
599
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
600
    return iter_revs
504
601
 
505
602
 
506
603
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
508
605
        # It's the tip
509
606
        return [(br_rev_id, br_revno, 0)]
510
607
    else:
511
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
512
 
        revno_str = '.'.join(str(n) for n in revno)
 
608
        revno_str = _compute_revno_str(branch, rev_id)
513
609
        return [(rev_id, revno_str, 0)]
514
610
 
515
611
 
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)):
522
 
        try:
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)
529
 
    return result
530
 
 
531
 
 
532
612
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
533
 
    delayed_graph_generation):
 
613
                            delayed_graph_generation,
 
614
                            exclude_common_ancestry=False):
534
615
    # On large trees, generating the merge graph can take 30-60 seconds
535
616
    # so we delay doing it until a merge is detected, incrementally
536
617
    # returning initial (non-merge) revisions while we can.
 
618
 
 
619
    # The above is only true for old formats (<= 0.92), for newer formats, a
 
620
    # couple of seconds only should be needed to load the whole graph and the
 
621
    # other graph operations needed are even faster than that -- vila 100201
537
622
    initial_revisions = []
538
623
    if delayed_graph_generation:
539
624
        try:
540
 
            for rev_id, revno, depth in \
541
 
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
625
            for rev_id, revno, depth in  _linear_view_revisions(
 
626
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
542
627
                if _has_merges(branch, rev_id):
 
628
                    # The end_rev_id can be nested down somewhere. We need an
 
629
                    # explicit ancestry check. There is an ambiguity here as we
 
630
                    # may not raise _StartNotLinearAncestor for a revision that
 
631
                    # is an ancestor but not a *linear* one. But since we have
 
632
                    # loaded the graph to do the check (or calculate a dotted
 
633
                    # revno), we may as well accept to show the log...  We need
 
634
                    # the check only if start_rev_id is not None as all
 
635
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
 
636
                    # -- vila 20100319
 
637
                    graph = branch.repository.get_graph()
 
638
                    if (start_rev_id is not None
 
639
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
640
                        raise _StartNotLinearAncestor()
 
641
                    # Since we collected the revisions so far, we need to
 
642
                    # adjust end_rev_id.
543
643
                    end_rev_id = rev_id
544
644
                    break
545
645
                else:
546
646
                    initial_revisions.append((rev_id, revno, depth))
547
647
            else:
548
648
                # No merged revisions found
549
 
                if direction == 'reverse':
550
 
                    return initial_revisions
551
 
                elif direction == 'forward':
552
 
                    return reversed(initial_revisions)
553
 
                else:
554
 
                    raise ValueError('invalid direction %r' % direction)
 
649
                return initial_revisions
555
650
        except _StartNotLinearAncestor:
556
651
            # A merge was never detected so the lower revision limit can't
557
652
            # be nested down somewhere
558
 
            raise errors.BzrCommandError('Start revision not found in'
559
 
                ' history of end revision.')
 
653
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
654
                ' history of end revision.'))
 
655
 
 
656
    # We exit the loop above because we encounter a revision with merges, from
 
657
    # this revision, we need to switch to _graph_view_revisions.
560
658
 
561
659
    # A log including nested merges is required. If the direction is reverse,
562
660
    # we rebase the initial merge depths so that the development line is
565
663
    # indented at the end seems slightly nicer in that case.
566
664
    view_revisions = chain(iter(initial_revisions),
567
665
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
568
 
        rebase_initial_depths=direction == 'reverse'))
569
 
    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)
575
 
    else:
576
 
        raise ValueError('invalid direction %r' % direction)
 
666
                              rebase_initial_depths=(direction == 'reverse'),
 
667
                              exclude_common_ancestry=exclude_common_ancestry))
 
668
    return view_revisions
577
669
 
578
670
 
579
671
def _has_merges(branch, rev_id):
582
674
    return len(parents) > 1
583
675
 
584
676
 
 
677
def _compute_revno_str(branch, rev_id):
 
678
    """Compute the revno string from a rev_id.
 
679
 
 
680
    :return: The revno string, or None if the revision is not in the supplied
 
681
        branch.
 
682
    """
 
683
    try:
 
684
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
685
    except errors.NoSuchRevision:
 
686
        # The revision must be outside of this branch
 
687
        return None
 
688
    else:
 
689
        return '.'.join(str(n) for n in revno)
 
690
 
 
691
 
585
692
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
586
693
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
587
694
    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)
 
695
        try:
 
696
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
697
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
698
        except errors.NoSuchRevision:
 
699
            # one or both is not in the branch; not obvious
 
700
            return False
590
701
        if len(start_dotted) == 1 and len(end_dotted) == 1:
591
702
            # both on mainline
592
703
            return start_dotted[0] <= end_dotted[0]
597
708
        else:
598
709
            # not obvious
599
710
            return False
 
711
    # if either start or end is not specified then we use either the first or
 
712
    # the last revision and *they* are obvious ancestors.
600
713
    return True
601
714
 
602
715
 
603
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
716
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
717
                           exclude_common_ancestry=False):
604
718
    """Calculate a sequence of revisions to view, newest to oldest.
605
719
 
606
720
    :param start_rev_id: the lower revision-id
607
721
    :param end_rev_id: the upper revision-id
 
722
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
723
        the iterated revisions.
608
724
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
609
725
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
610
 
      is not found walking the left-hand history
 
726
        is not found walking the left-hand history
611
727
    """
612
728
    br_revno, br_rev_id = branch.last_revision_info()
613
729
    repo = branch.repository
 
730
    graph = repo.get_graph()
614
731
    if start_rev_id is None and end_rev_id is None:
615
732
        cur_revno = br_revno
616
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
733
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
734
            (_mod_revision.NULL_REVISION,)):
617
735
            yield revision_id, str(cur_revno), 0
618
736
            cur_revno -= 1
619
737
    else:
620
738
        if end_rev_id is None:
621
739
            end_rev_id = br_rev_id
622
740
        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)
 
741
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
742
                (_mod_revision.NULL_REVISION,)):
 
743
            revno_str = _compute_revno_str(branch, revision_id)
626
744
            if not found_start and revision_id == start_rev_id:
627
 
                yield revision_id, revno_str, 0
 
745
                if not exclude_common_ancestry:
 
746
                    yield revision_id, revno_str, 0
628
747
                found_start = True
629
748
                break
630
749
            else:
635
754
 
636
755
 
637
756
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
638
 
    rebase_initial_depths=True):
 
757
                          rebase_initial_depths=True,
 
758
                          exclude_common_ancestry=False):
639
759
    """Calculate revisions to view including merges, newest to oldest.
640
760
 
641
761
    :param branch: the branch
645
765
      revision is found?
646
766
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
647
767
    """
 
768
    if exclude_common_ancestry:
 
769
        stop_rule = 'with-merges-without-common-ancestry'
 
770
    else:
 
771
        stop_rule = 'with-merges'
648
772
    view_revisions = branch.iter_merge_sorted_revisions(
649
773
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
650
 
        stop_rule="with-merges")
 
774
        stop_rule=stop_rule)
651
775
    if not rebase_initial_depths:
652
776
        for (rev_id, merge_depth, revno, end_of_merge
653
777
             ) in view_revisions:
664
788
                depth_adjustment = merge_depth
665
789
            if depth_adjustment:
666
790
                if merge_depth < depth_adjustment:
 
791
                    # From now on we reduce the depth adjustement, this can be
 
792
                    # surprising for users. The alternative requires two passes
 
793
                    # which breaks the fast display of the first revision
 
794
                    # though.
667
795
                    depth_adjustment = merge_depth
668
796
                merge_depth -= depth_adjustment
669
797
            yield rev_id, '.'.join(map(str, revno)), merge_depth
670
798
 
671
799
 
672
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
673
 
        specific_fileid, generate_merge_revisions):
674
 
    """Calculate the revisions to view.
675
 
 
676
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
677
 
             a list of the same tuples.
678
 
    """
679
 
    # This method is no longer called by the main code path.
680
 
    # It is retained for API compatibility and may be deprecated
681
 
    # soon. IGC 20090116
682
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
683
 
        end_revision)
684
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
685
 
        direction, generate_merge_revisions or specific_fileid))
686
 
    if specific_fileid:
687
 
        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)
691
 
 
692
 
 
693
800
def _rebase_merge_depth(view_revisions):
694
801
    """Adjust depths upwards so the top level is 0."""
695
802
    # If either the first or last revision have a merge_depth of 0, we're done
739
846
    return log_rev_iterator
740
847
 
741
848
 
742
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
849
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
743
850
    """Create a filtered iterator of log_rev_iterator matching on a regex.
744
851
 
745
852
    :param branch: The branch being logged.
746
853
    :param generate_delta: Whether to generate a delta for each revision.
747
 
    :param search: A user text search string.
 
854
    :param match: A dictionary with properties as keys and lists of strings
 
855
        as values. To match, a revision may match any of the supplied strings
 
856
        within a single property but must match at least one string for each
 
857
        property.
748
858
    :param log_rev_iterator: An input iterator containing all revisions that
749
859
        could be displayed, in lists.
750
860
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
751
861
        delta).
752
862
    """
753
 
    if search is None:
 
863
    if match is None:
754
864
        return log_rev_iterator
755
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
756
 
            'log message filter')
757
 
    return _filter_message_re(searchRE, log_rev_iterator)
758
 
 
759
 
 
760
 
def _filter_message_re(searchRE, log_rev_iterator):
 
865
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
866
                for (k,v) in match.iteritems()]
 
867
    return _filter_re(searchRE, log_rev_iterator)
 
868
 
 
869
 
 
870
def _filter_re(searchRE, log_rev_iterator):
761
871
    for revs in log_rev_iterator:
762
 
        new_revs = []
763
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
764
 
            if searchRE.search(rev.message):
765
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
766
 
        yield new_revs
767
 
 
 
872
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
873
        if new_revs:
 
874
            yield new_revs
 
875
 
 
876
def _match_filter(searchRE, rev):
 
877
    strings = {
 
878
               'message': (rev.message,),
 
879
               'committer': (rev.committer,),
 
880
               'author': (rev.get_apparent_authors()),
 
881
               'bugs': list(rev.iter_bugs())
 
882
               }
 
883
    strings[''] = [item for inner_list in strings.itervalues()
 
884
                   for item in inner_list]
 
885
    for (k,v) in searchRE:
 
886
        if k in strings and not _match_any_filter(strings[k], v):
 
887
            return False
 
888
    return True
 
889
 
 
890
def _match_any_filter(strings, res):
 
891
    return any([filter(None, map(re.search, strings)) for re in res])
768
892
 
769
893
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
770
894
    fileids=None, direction='reverse'):
843
967
 
844
968
def _update_fileids(delta, fileids, stop_on):
845
969
    """Update the set of file-ids to search based on file lifecycle events.
846
 
    
 
970
 
847
971
    :param fileids: a set of fileids to update
848
972
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
849
973
      fileids set once their add or remove entry is detected respectively
890
1014
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
891
1015
        delta).
892
1016
    """
893
 
    repository = branch.repository
894
1017
    num = 9
895
1018
    for batch in log_rev_iterator:
896
1019
        batch = iter(batch)
945
1068
    if branch_revno != 0:
946
1069
        if (start_rev_id == _mod_revision.NULL_REVISION
947
1070
            or end_rev_id == _mod_revision.NULL_REVISION):
948
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1071
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
949
1072
        if start_revno > end_revno:
950
 
            raise errors.BzrCommandError("Start revision must be older than "
951
 
                                         "the end revision.")
 
1073
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1074
                                         "older than the end revision."))
952
1075
    return (start_rev_id, end_rev_id)
953
1076
 
954
1077
 
1003
1126
 
1004
1127
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1005
1128
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1006
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1129
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1007
1130
    if start_revno > end_revno:
1008
 
        raise errors.BzrCommandError("Start revision must be older than "
1009
 
                                     "the end revision.")
 
1131
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1132
                                     "than the end revision."))
1010
1133
 
1011
1134
    if end_revno < start_revno:
1012
1135
        return None, None, None, None
1013
1136
    cur_revno = branch_revno
1014
1137
    rev_nos = {}
1015
1138
    mainline_revs = []
1016
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1017
 
                        branch_last_revision):
 
1139
    graph = branch.repository.get_graph()
 
1140
    for revision_id in graph.iter_lefthand_ancestry(
 
1141
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1018
1142
        if cur_revno < start_revno:
1019
1143
            # We have gone far enough, but we always add 1 more revision
1020
1144
            rev_nos[revision_id] = cur_revno
1034
1158
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1035
1159
 
1036
1160
 
1037
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1038
 
    """Filter view_revisions based on revision ranges.
1039
 
 
1040
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1041
 
            tuples to be filtered.
1042
 
 
1043
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1044
 
            If NONE then all revisions up to the end_rev_id are logged.
1045
 
 
1046
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1047
 
            If NONE then all revisions up to the end of the log are logged.
1048
 
 
1049
 
    :return: The filtered view_revisions.
1050
 
    """
1051
 
    # This method is no longer called by the main code path.
1052
 
    # It may be removed soon. IGC 20090127
1053
 
    if start_rev_id or end_rev_id:
1054
 
        revision_ids = [r for r, n, d in view_revisions]
1055
 
        if start_rev_id:
1056
 
            start_index = revision_ids.index(start_rev_id)
1057
 
        else:
1058
 
            start_index = 0
1059
 
        if start_rev_id == end_rev_id:
1060
 
            end_index = start_index
1061
 
        else:
1062
 
            if end_rev_id:
1063
 
                end_index = revision_ids.index(end_rev_id)
1064
 
            else:
1065
 
                end_index = len(view_revisions) - 1
1066
 
        # To include the revisions merged into the last revision,
1067
 
        # extend end_rev_id down to, but not including, the next rev
1068
 
        # with the same or lesser merge_depth
1069
 
        end_merge_depth = view_revisions[end_index][2]
1070
 
        try:
1071
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1072
 
                if view_revisions[index][2] <= end_merge_depth:
1073
 
                    end_index = index - 1
1074
 
                    break
1075
 
        except IndexError:
1076
 
            # if the search falls off the end then log to the end as well
1077
 
            end_index = len(view_revisions) - 1
1078
 
        view_revisions = view_revisions[start_index:end_index+1]
1079
 
    return view_revisions
1080
 
 
1081
 
 
1082
1161
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1083
1162
    include_merges=True):
1084
1163
    r"""Return the list of revision ids which touch a given file id.
1087
1166
    This includes the revisions which directly change the file id,
1088
1167
    and the revisions which merge these changes. So if the
1089
1168
    revision graph is::
 
1169
 
1090
1170
        A-.
1091
1171
        |\ \
1092
1172
        B C E
1119
1199
    """
1120
1200
    # Lookup all possible text keys to determine which ones actually modified
1121
1201
    # the file.
 
1202
    graph = branch.repository.get_file_graph()
 
1203
    get_parent_map = graph.get_parent_map
1122
1204
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1123
1205
    next_keys = None
1124
1206
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1128
1210
    #       indexing layer. We might consider passing in hints as to the known
1129
1211
    #       access pattern (sparse/clustered, high success rate/low success
1130
1212
    #       rate). This particular access is clustered with a low success rate.
1131
 
    get_parent_map = branch.repository.texts.get_parent_map
1132
1213
    modified_text_revisions = set()
1133
1214
    chunk_size = 1000
1134
1215
    for start in xrange(0, len(text_keys), chunk_size):
1161
1242
    return result
1162
1243
 
1163
1244
 
1164
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1165
 
                       include_merges=True):
1166
 
    """Produce an iterator of revisions to show
1167
 
    :return: an iterator of (revision_id, revno, merge_depth)
1168
 
    (if there is no revno for a revision, None is supplied)
1169
 
    """
1170
 
    # This method is no longer called by the main code path.
1171
 
    # It is retained for API compatibility and may be deprecated
1172
 
    # soon. IGC 20090127
1173
 
    if not include_merges:
1174
 
        revision_ids = mainline_revs[1:]
1175
 
        if direction == 'reverse':
1176
 
            revision_ids.reverse()
1177
 
        for revision_id in revision_ids:
1178
 
            yield revision_id, str(rev_nos[revision_id]), 0
1179
 
        return
1180
 
    graph = branch.repository.get_graph()
1181
 
    # This asks for all mainline revisions, which means we only have to spider
1182
 
    # sideways, rather than depth history. That said, its still size-of-history
1183
 
    # and should be addressed.
1184
 
    # mainline_revisions always includes an extra revision at the beginning, so
1185
 
    # don't request it.
1186
 
    parent_map = dict(((key, value) for key, value in
1187
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1188
 
    # filter out ghosts; merge_sort errors on ghosts.
1189
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1190
 
    merge_sorted_revisions = tsort.merge_sort(
1191
 
        rev_graph,
1192
 
        mainline_revs[-1],
1193
 
        mainline_revs,
1194
 
        generate_revno=True)
1195
 
 
1196
 
    if direction == 'forward':
1197
 
        # forward means oldest first.
1198
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1199
 
    elif direction != 'reverse':
1200
 
        raise ValueError('invalid direction %r' % direction)
1201
 
 
1202
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1203
 
         ) in merge_sorted_revisions:
1204
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1205
 
 
1206
 
 
1207
1245
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1208
1246
    """Reverse revisions by depth.
1209
1247
 
1244
1282
    """
1245
1283
 
1246
1284
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1247
 
                 tags=None, diff=None):
 
1285
                 tags=None, diff=None, signature=None):
1248
1286
        self.rev = rev
1249
 
        self.revno = str(revno)
 
1287
        if revno is None:
 
1288
            self.revno = None
 
1289
        else:
 
1290
            self.revno = str(revno)
1250
1291
        self.merge_depth = merge_depth
1251
1292
        self.delta = delta
1252
1293
        self.tags = tags
1253
1294
        self.diff = diff
 
1295
        self.signature = signature
1254
1296
 
1255
1297
 
1256
1298
class LogFormatter(object):
1265
1307
    to indicate which LogRevision attributes it supports:
1266
1308
 
1267
1309
    - supports_delta must be True if this log formatter supports delta.
1268
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1269
 
        attribute describes whether the 'short_status' format (1) or the long
1270
 
        one (2) should be used.
 
1310
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1311
      attribute describes whether the 'short_status' format (1) or the long
 
1312
      one (2) should be used.
1271
1313
 
1272
1314
    - supports_merge_revisions must be True if this log formatter supports
1273
 
        merge revisions.  If not, then only mainline revisions will be passed
1274
 
        to the formatter.
 
1315
      merge revisions.  If not, then only mainline revisions will be passed
 
1316
      to the formatter.
1275
1317
 
1276
1318
    - preferred_levels is the number of levels this formatter defaults to.
1277
 
        The default value is zero meaning display all levels.
1278
 
        This value is only relevant if supports_merge_revisions is True.
 
1319
      The default value is zero meaning display all levels.
 
1320
      This value is only relevant if supports_merge_revisions is True.
1279
1321
 
1280
1322
    - supports_tags must be True if this log formatter supports tags.
1281
 
        Otherwise the tags attribute may not be populated.
 
1323
      Otherwise the tags attribute may not be populated.
1282
1324
 
1283
1325
    - supports_diff must be True if this log formatter supports diffs.
1284
 
        Otherwise the diff attribute may not be populated.
 
1326
      Otherwise the diff attribute may not be populated.
 
1327
 
 
1328
    - supports_signatures must be True if this log formatter supports GPG
 
1329
      signatures.
1285
1330
 
1286
1331
    Plugins can register functions to show custom revision properties using
1287
1332
    the properties_handler_registry. The registered function
1288
 
    must respect the following interface description:
 
1333
    must respect the following interface description::
 
1334
 
1289
1335
        def my_show_properties(properties_dict):
1290
1336
            # code that returns a dict {'name':'value'} of the properties
1291
1337
            # to be shown
1293
1339
    preferred_levels = 0
1294
1340
 
1295
1341
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1296
 
            delta_format=None, levels=None, show_advice=False,
1297
 
            to_exact_file=None):
 
1342
                 delta_format=None, levels=None, show_advice=False,
 
1343
                 to_exact_file=None, author_list_handler=None):
1298
1344
        """Create a LogFormatter.
1299
1345
 
1300
1346
        :param to_file: the file to output to
1301
 
        :param to_exact_file: if set, gives an output stream to which 
 
1347
        :param to_exact_file: if set, gives an output stream to which
1302
1348
             non-Unicode diffs are written.
1303
1349
        :param show_ids: if True, revision-ids are to be displayed
1304
1350
        :param show_timezone: the timezone to use
1308
1354
          let the log formatter decide.
1309
1355
        :param show_advice: whether to show advice at the end of the
1310
1356
          log or not
 
1357
        :param author_list_handler: callable generating a list of
 
1358
          authors to display for a given revision
1311
1359
        """
1312
1360
        self.to_file = to_file
1313
1361
        # 'exact' stream used to show diff, it should print content 'as is'
1328
1376
        self.levels = levels
1329
1377
        self._show_advice = show_advice
1330
1378
        self._merge_count = 0
 
1379
        self._author_list_handler = author_list_handler
1331
1380
 
1332
1381
    def get_levels(self):
1333
1382
        """Get the number of levels to display or 0 for all."""
1352
1401
            if advice_sep:
1353
1402
                self.to_file.write(advice_sep)
1354
1403
            self.to_file.write(
1355
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1404
                "Use --include-merged or -n0 to see merged revisions.\n")
1356
1405
 
1357
1406
    def get_advice_separator(self):
1358
1407
        """Get the text separating the log from the closing advice."""
1365
1414
        return address
1366
1415
 
1367
1416
    def short_author(self, rev):
1368
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1369
 
        if name:
1370
 
            return name
1371
 
        return address
 
1417
        return self.authors(rev, 'first', short=True, sep=', ')
 
1418
 
 
1419
    def authors(self, rev, who, short=False, sep=None):
 
1420
        """Generate list of authors, taking --authors option into account.
 
1421
 
 
1422
        The caller has to specify the name of a author list handler,
 
1423
        as provided by the author list registry, using the ``who``
 
1424
        argument.  That name only sets a default, though: when the
 
1425
        user selected a different author list generation using the
 
1426
        ``--authors`` command line switch, as represented by the
 
1427
        ``author_list_handler`` constructor argument, that value takes
 
1428
        precedence.
 
1429
 
 
1430
        :param rev: The revision for which to generate the list of authors.
 
1431
        :param who: Name of the default handler.
 
1432
        :param short: Whether to shorten names to either name or address.
 
1433
        :param sep: What separator to use for automatic concatenation.
 
1434
        """
 
1435
        if self._author_list_handler is not None:
 
1436
            # The user did specify --authors, which overrides the default
 
1437
            author_list_handler = self._author_list_handler
 
1438
        else:
 
1439
            # The user didn't specify --authors, so we use the caller's default
 
1440
            author_list_handler = author_list_registry.get(who)
 
1441
        names = author_list_handler(rev)
 
1442
        if short:
 
1443
            for i in range(len(names)):
 
1444
                name, address = config.parse_username(names[i])
 
1445
                if name:
 
1446
                    names[i] = name
 
1447
                else:
 
1448
                    names[i] = address
 
1449
        if sep is not None:
 
1450
            names = sep.join(names)
 
1451
        return names
1372
1452
 
1373
1453
    def merge_marker(self, revision):
1374
1454
        """Get the merge marker to include in the output or '' if none."""
1405
1485
        """
1406
1486
        # Revision comes directly from a foreign repository
1407
1487
        if isinstance(rev, foreign.ForeignRevision):
1408
 
            return rev.mapping.vcs.show_foreign_revid(rev.foreign_revid)
 
1488
            return self._format_properties(
 
1489
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1409
1490
 
1410
1491
        # Imported foreign revision revision ids always contain :
1411
1492
        if not ":" in rev.revision_id:
1443
1524
    supports_delta = True
1444
1525
    supports_tags = True
1445
1526
    supports_diff = True
 
1527
    supports_signatures = True
1446
1528
 
1447
1529
    def __init__(self, *args, **kwargs):
1448
1530
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1468
1550
                self.merge_marker(revision)))
1469
1551
        if revision.tags:
1470
1552
            lines.append('tags: %s' % (', '.join(revision.tags)))
1471
 
        if self.show_ids:
 
1553
        if self.show_ids or revision.revno is None:
1472
1554
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1555
        if self.show_ids:
1473
1556
            for parent_id in revision.rev.parent_ids:
1474
1557
                lines.append('parent: %s' % (parent_id,))
1475
1558
        lines.extend(self.custom_properties(revision.rev))
1476
1559
 
1477
1560
        committer = revision.rev.committer
1478
 
        authors = revision.rev.get_apparent_authors()
 
1561
        authors = self.authors(revision.rev, 'all')
1479
1562
        if authors != [committer]:
1480
1563
            lines.append('author: %s' % (", ".join(authors),))
1481
1564
        lines.append('committer: %s' % (committer,))
1486
1569
 
1487
1570
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1488
1571
 
 
1572
        if revision.signature is not None:
 
1573
            lines.append('signature: ' + revision.signature)
 
1574
 
1489
1575
        lines.append('message:')
1490
1576
        if not revision.rev.message:
1491
1577
            lines.append('  (no message)')
1498
1584
        to_file = self.to_file
1499
1585
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1500
1586
        if revision.delta is not None:
1501
 
            # We don't respect delta_format for compatibility
1502
 
            revision.delta.show(to_file, self.show_ids, indent=indent,
1503
 
                                short_status=False)
 
1587
            # Use the standard status output to display changes
 
1588
            from bzrlib.delta import report_delta
 
1589
            report_delta(to_file, revision.delta, short_status=False,
 
1590
                         show_ids=self.show_ids, indent=indent)
1504
1591
        if revision.diff is not None:
1505
1592
            to_file.write(indent + 'diff:\n')
1506
1593
            to_file.flush()
1537
1624
        indent = '    ' * depth
1538
1625
        revno_width = self.revno_width_by_depth.get(depth)
1539
1626
        if revno_width is None:
1540
 
            if revision.revno.find('.') == -1:
 
1627
            if revision.revno is None or revision.revno.find('.') == -1:
1541
1628
                # mainline revno, e.g. 12345
1542
1629
                revno_width = 5
1543
1630
            else:
1551
1638
        if revision.tags:
1552
1639
            tags = ' {%s}' % (', '.join(revision.tags))
1553
1640
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1554
 
                revision.revno, self.short_author(revision.rev),
 
1641
                revision.revno or "", self.short_author(revision.rev),
1555
1642
                format_date(revision.rev.timestamp,
1556
1643
                            revision.rev.timezone or 0,
1557
1644
                            self.show_timezone, date_fmt="%Y-%m-%d",
1558
1645
                            show_offset=False),
1559
1646
                tags, self.merge_marker(revision)))
1560
1647
        self.show_properties(revision.rev, indent+offset)
1561
 
        if self.show_ids:
 
1648
        if self.show_ids or revision.revno is None:
1562
1649
            to_file.write(indent + offset + 'revision-id:%s\n'
1563
1650
                          % (revision.rev.revision_id,))
1564
1651
        if not revision.rev.message:
1569
1656
                to_file.write(indent + offset + '%s\n' % (l,))
1570
1657
 
1571
1658
        if revision.delta is not None:
1572
 
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
1573
 
                                short_status=self.delta_format==1)
 
1659
            # Use the standard status output to display changes
 
1660
            from bzrlib.delta import report_delta
 
1661
            report_delta(to_file, revision.delta,
 
1662
                         short_status=self.delta_format==1,
 
1663
                         show_ids=self.show_ids, indent=indent + offset)
1574
1664
        if revision.diff is not None:
1575
1665
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1576
1666
        to_file.write('\n')
1614
1704
 
1615
1705
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1616
1706
        """Format log info into one string. Truncate tail of string
1617
 
        :param  revno:      revision number or None.
1618
 
                            Revision numbers counts from 1.
1619
 
        :param  rev:        revision object
1620
 
        :param  max_chars:  maximum length of resulting string
1621
 
        :param  tags:       list of tags or None
1622
 
        :param  prefix:     string to prefix each line
1623
 
        :return:            formatted truncated string
 
1707
 
 
1708
        :param revno:      revision number or None.
 
1709
                           Revision numbers counts from 1.
 
1710
        :param rev:        revision object
 
1711
        :param max_chars:  maximum length of resulting string
 
1712
        :param tags:       list of tags or None
 
1713
        :param prefix:     string to prefix each line
 
1714
        :return:           formatted truncated string
1624
1715
        """
1625
1716
        out = []
1626
1717
        if revno:
1627
1718
            # show revno only when is not None
1628
1719
            out.append("%s:" % revno)
1629
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1720
        if max_chars is not None:
 
1721
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1722
        else:
 
1723
            out.append(self.short_author(rev))
1630
1724
        out.append(self.date_string(rev))
1631
1725
        if len(rev.parent_ids) > 1:
1632
1726
            out.append('[merge]')
1651
1745
                               self.show_timezone,
1652
1746
                               date_fmt='%Y-%m-%d',
1653
1747
                               show_offset=False)
1654
 
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1748
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1749
        committer_str = committer_str.replace(' <', '  <')
1655
1750
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1656
1751
 
1657
1752
        if revision.delta is not None and revision.delta.has_changed():
1690
1785
        return self.get(name)(*args, **kwargs)
1691
1786
 
1692
1787
    def get_default(self, branch):
1693
 
        return self.get(branch.get_config().log_format())
 
1788
        c = branch.get_config_stack()
 
1789
        return self.get(c.get('log_format'))
1694
1790
 
1695
1791
 
1696
1792
log_formatter_registry = LogFormatterRegistry()
1697
1793
 
1698
1794
 
1699
1795
log_formatter_registry.register('short', ShortLogFormatter,
1700
 
                                'Moderately short log format')
 
1796
                                'Moderately short log format.')
1701
1797
log_formatter_registry.register('long', LongLogFormatter,
1702
 
                                'Detailed log format')
 
1798
                                'Detailed log format.')
1703
1799
log_formatter_registry.register('line', LineLogFormatter,
1704
 
                                'Log format with one line per revision')
 
1800
                                'Log format with one line per revision.')
1705
1801
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1706
 
                                'Format used by GNU ChangeLog files')
 
1802
                                'Format used by GNU ChangeLog files.')
1707
1803
 
1708
1804
 
1709
1805
def register_formatter(name, formatter):
1719
1815
    try:
1720
1816
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1721
1817
    except KeyError:
1722
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1723
 
 
1724
 
 
1725
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1726
 
    # deprecated; for compatibility
1727
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1728
 
    lf.show(revno, rev, delta)
 
1818
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1819
 
 
1820
 
 
1821
def author_list_all(rev):
 
1822
    return rev.get_apparent_authors()[:]
 
1823
 
 
1824
 
 
1825
def author_list_first(rev):
 
1826
    lst = rev.get_apparent_authors()
 
1827
    try:
 
1828
        return [lst[0]]
 
1829
    except IndexError:
 
1830
        return []
 
1831
 
 
1832
 
 
1833
def author_list_committer(rev):
 
1834
    return [rev.committer]
 
1835
 
 
1836
 
 
1837
author_list_registry = registry.Registry()
 
1838
 
 
1839
author_list_registry.register('all', author_list_all,
 
1840
                              'All authors')
 
1841
 
 
1842
author_list_registry.register('first', author_list_first,
 
1843
                              'The first author')
 
1844
 
 
1845
author_list_registry.register('committer', author_list_committer,
 
1846
                              'The committer')
1729
1847
 
1730
1848
 
1731
1849
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1796
1914
    old_revisions = set()
1797
1915
    new_history = []
1798
1916
    new_revisions = set()
1799
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1800
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1917
    graph = repository.get_graph()
 
1918
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1919
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1801
1920
    stop_revision = None
1802
1921
    do_old = True
1803
1922
    do_new = True
1878
1997
        lf.log_revision(lr)
1879
1998
 
1880
1999
 
1881
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2000
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1882
2001
    """Find file-ids and kinds given a list of files and a revision range.
1883
2002
 
1884
2003
    We search for files at the end of the range. If not found there,
1888
2007
    :param file_list: the list of paths given on the command line;
1889
2008
      the first of these can be a branch location or a file path,
1890
2009
      the remainder must be file paths
 
2010
    :param add_cleanup: When the branch returned is read locked,
 
2011
      an unlock call will be queued to the cleanup.
1891
2012
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1892
2013
      info_list is a list of (relative_path, file_id, kind) tuples where
1893
2014
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1894
2015
      branch will be read-locked.
1895
2016
    """
1896
 
    from builtins import _get_revision_range, safe_relpath_files
1897
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1898
 
    b.lock_read()
 
2017
    from bzrlib.builtins import _get_revision_range
 
2018
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2019
        file_list[0])
 
2020
    add_cleanup(b.lock_read().unlock)
1899
2021
    # XXX: It's damn messy converting a list of paths to relative paths when
1900
2022
    # those paths might be deleted ones, they might be on a case-insensitive
1901
2023
    # filesystem and/or they might be in silly locations (like another branch).
1905
2027
    # case of running log in a nested directory, assuming paths beyond the
1906
2028
    # first one haven't been deleted ...
1907
2029
    if tree:
1908
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2030
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1909
2031
    else:
1910
2032
        relpaths = [path] + file_list[1:]
1911
2033
    info_list = []
1987
2109
        bug_rows = [line.split(' ', 1) for line in bug_lines]
1988
2110
        fixed_bug_urls = [row[0] for row in bug_rows if
1989
2111
                          len(row) > 1 and row[1] == 'fixed']
1990
 
        
 
2112
 
1991
2113
        if fixed_bug_urls:
1992
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2114
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2115
                    ' '.join(fixed_bug_urls)}
1993
2116
    return {}
1994
2117
 
1995
2118
properties_handler_registry.register('bugs_properties_handler',