~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jelmer Vernooij
  • Date: 2012-02-20 14:15:25 UTC
  • mto: (6471.1.4 iter-child-entries)
  • mto: This revision was merged to the branch mainline in revision 6472.
  • Revision ID: jelmer@samba.org-20120220141525-9azkfei62st8yc7w
Use inventories directly in fewer places.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
94
 
    )
95
92
 
96
93
 
97
94
def find_touching_revisions(branch, file_id):
108
105
    last_ie = None
109
106
    last_path = None
110
107
    revno = 1
111
 
    for revision_id in branch.revision_history():
112
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
113
 
        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):
114
114
            this_ie = this_inv[file_id]
115
115
            this_path = this_inv.id2path(file_id)
116
116
        else:
156
156
             end_revision=None,
157
157
             search=None,
158
158
             limit=None,
159
 
             show_diff=False):
 
159
             show_diff=False,
 
160
             match=None):
160
161
    """Write out human-readable log of commits to this branch.
161
162
 
162
163
    This function is being retained for backwards compatibility but
185
186
        if None or 0.
186
187
 
187
188
    :param show_diff: If True, output a diff after each revision.
 
189
 
 
190
    :param match: Dictionary of search lists to use when matching revision
 
191
      properties.
188
192
    """
189
193
    # Convert old-style parameters to new-style parameters
190
194
    if specific_fileid is not None:
214
218
    Logger(branch, rqst).show(lf)
215
219
 
216
220
 
217
 
# Note: This needs to be kept this in sync with the defaults in
 
221
# Note: This needs to be kept in sync with the defaults in
218
222
# make_log_request_dict() below
219
223
_DEFAULT_REQUEST_PARAMS = {
220
224
    'direction': 'reverse',
221
 
    'levels': 1,
 
225
    'levels': None,
222
226
    'generate_tags': True,
 
227
    'exclude_common_ancestry': False,
223
228
    '_match_using_deltas': True,
224
229
    }
225
230
 
226
231
 
227
232
def make_log_request_dict(direction='reverse', specific_fileids=None,
228
 
    start_revision=None, end_revision=None, limit=None,
229
 
    message_search=None, levels=1, generate_tags=True, delta_type=None,
230
 
    diff_type=None, _match_using_deltas=True):
 
233
                          start_revision=None, end_revision=None, limit=None,
 
234
                          message_search=None, levels=None, generate_tags=True,
 
235
                          delta_type=None,
 
236
                          diff_type=None, _match_using_deltas=True,
 
237
                          exclude_common_ancestry=False, match=None,
 
238
                          signature=False, omit_merges=False,
 
239
                          ):
231
240
    """Convenience function for making a logging request dictionary.
232
241
 
233
242
    Using this function may make code slightly safer by ensuring
253
262
      matching commit messages
254
263
 
255
264
    :param levels: the number of levels of revisions to
256
 
      generate; 1 for just the mainline; 0 for all levels.
 
265
      generate; 1 for just the mainline; 0 for all levels, or None for
 
266
      a sensible default.
257
267
 
258
268
    :param generate_tags: If True, include tags for matched revisions.
259
 
 
 
269
`
260
270
    :param delta_type: Either 'full', 'partial' or None.
261
271
      'full' means generate the complete delta - adds/deletes/modifies/etc;
262
272
      'partial' means filter the delta using specific_fileids;
271
281
      algorithm used for matching specific_fileids. This parameter
272
282
      may be removed in the future so bzrlib client code should NOT
273
283
      use it.
 
284
 
 
285
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
 
286
      range operator or as a graph difference.
 
287
 
 
288
    :param signature: show digital signature information
 
289
 
 
290
    :param match: Dictionary of list of search strings to use when filtering
 
291
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
292
      the empty string to match any of the preceding properties.
 
293
 
 
294
    :param omit_merges: If True, commits with more than one parent are
 
295
      omitted.
 
296
 
274
297
    """
 
298
    # Take care of old style message_search parameter
 
299
    if message_search:
 
300
        if match:
 
301
            if 'message' in match:
 
302
                match['message'].append(message_search)
 
303
            else:
 
304
                match['message'] = [message_search]
 
305
        else:
 
306
            match={ 'message': [message_search] }
275
307
    return {
276
308
        'direction': direction,
277
309
        'specific_fileids': specific_fileids,
278
310
        'start_revision': start_revision,
279
311
        'end_revision': end_revision,
280
312
        'limit': limit,
281
 
        'message_search': message_search,
282
313
        'levels': levels,
283
314
        'generate_tags': generate_tags,
284
315
        'delta_type': delta_type,
285
316
        'diff_type': diff_type,
 
317
        'exclude_common_ancestry': exclude_common_ancestry,
 
318
        'signature': signature,
 
319
        'match': match,
 
320
        'omit_merges': omit_merges,
286
321
        # Add 'private' attributes for features that may be deprecated
287
322
        '_match_using_deltas': _match_using_deltas,
288
323
    }
290
325
 
291
326
def _apply_log_request_defaults(rqst):
292
327
    """Apply default values to a request dictionary."""
293
 
    result = _DEFAULT_REQUEST_PARAMS
 
328
    result = _DEFAULT_REQUEST_PARAMS.copy()
294
329
    if rqst:
295
330
        result.update(rqst)
296
331
    return result
297
332
 
298
333
 
 
334
def format_signature_validity(rev_id, repo):
 
335
    """get the signature validity
 
336
 
 
337
    :param rev_id: revision id to validate
 
338
    :param repo: repository of revision
 
339
    :return: human readable string to print to log
 
340
    """
 
341
    from bzrlib import gpg
 
342
 
 
343
    gpg_strategy = gpg.GPGStrategy(None)
 
344
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
 
345
    if result[0] == gpg.SIGNATURE_VALID:
 
346
        return "valid signature from {0}".format(result[1])
 
347
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
348
        return "unknown key {0}".format(result[1])
 
349
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
350
        return "invalid signature!"
 
351
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
352
        return "no signature"
 
353
 
 
354
 
299
355
class LogGenerator(object):
300
356
    """A generator of log revisions."""
301
357
 
346
402
        # Tweak the LogRequest based on what the LogFormatter can handle.
347
403
        # (There's no point generating stuff if the formatter can't display it.)
348
404
        rqst = self.rqst
349
 
        rqst['levels'] = lf.get_levels()
 
405
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
406
            # user didn't specify levels, use whatever the LF can handle:
 
407
            rqst['levels'] = lf.get_levels()
 
408
 
350
409
        if not getattr(lf, 'supports_tags', False):
351
410
            rqst['generate_tags'] = False
352
411
        if not getattr(lf, 'supports_delta', False):
353
412
            rqst['delta_type'] = None
354
413
        if not getattr(lf, 'supports_diff', False):
355
414
            rqst['diff_type'] = None
 
415
        if not getattr(lf, 'supports_signatures', False):
 
416
            rqst['signature'] = False
356
417
 
357
418
        # Find and print the interesting revisions
358
419
        generator = self._generator_factory(self.branch, rqst)
362
423
 
363
424
    def _generator_factory(self, branch, rqst):
364
425
        """Make the LogGenerator object to use.
365
 
        
 
426
 
366
427
        Subclasses may wish to override this.
367
428
        """
368
429
        return _DefaultLogGenerator(branch, rqst)
392
453
        levels = rqst.get('levels')
393
454
        limit = rqst.get('limit')
394
455
        diff_type = rqst.get('diff_type')
 
456
        show_signature = rqst.get('signature')
 
457
        omit_merges = rqst.get('omit_merges')
395
458
        log_count = 0
396
459
        revision_iterator = self._create_log_revision_iterator()
397
460
        for revs in revision_iterator:
399
462
                # 0 levels means show everything; merge_depth counts from 0
400
463
                if levels != 0 and merge_depth >= levels:
401
464
                    continue
 
465
                if omit_merges and len(rev.parent_ids) > 1:
 
466
                    continue
402
467
                if diff_type is None:
403
468
                    diff = None
404
469
                else:
405
470
                    diff = self._format_diff(rev, rev_id, diff_type)
 
471
                if show_signature:
 
472
                    signature = format_signature_validity(rev_id,
 
473
                                                self.branch.repository)
 
474
                else:
 
475
                    signature = None
406
476
                yield LogRevision(rev, revno, merge_depth, delta,
407
 
                    self.rev_tag_dict.get(rev_id), diff)
 
477
                    self.rev_tag_dict.get(rev_id), diff, signature)
408
478
                if limit:
409
479
                    log_count += 1
410
480
                    if log_count >= limit:
424
494
        else:
425
495
            specific_files = None
426
496
        s = StringIO()
 
497
        path_encoding = get_diff_header_encoding()
427
498
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
428
 
            new_label='')
 
499
            new_label='', path_encoding=path_encoding)
429
500
        return s.getvalue()
430
501
 
431
502
    def _create_log_revision_iterator(self):
455
526
        generate_merge_revisions = rqst.get('levels') != 1
456
527
        delayed_graph_generation = not rqst.get('specific_fileids') and (
457
528
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
458
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
459
 
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
460
 
            delayed_graph_generation=delayed_graph_generation)
 
529
        view_revisions = _calc_view_revisions(
 
530
            self.branch, self.start_rev_id, self.end_rev_id,
 
531
            rqst.get('direction'),
 
532
            generate_merge_revisions=generate_merge_revisions,
 
533
            delayed_graph_generation=delayed_graph_generation,
 
534
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
461
535
 
462
536
        # Apply the other filters
463
537
        return make_log_rev_iterator(self.branch, view_revisions,
464
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
538
            rqst.get('delta_type'), rqst.get('match'),
465
539
            file_ids=rqst.get('specific_fileids'),
466
540
            direction=rqst.get('direction'))
467
541
 
470
544
        # Note that we always generate the merge revisions because
471
545
        # filter_revisions_touching_file_id() requires them ...
472
546
        rqst = self.rqst
473
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
474
 
            self.end_rev_id, rqst.get('direction'), True)
 
547
        view_revisions = _calc_view_revisions(
 
548
            self.branch, self.start_rev_id, self.end_rev_id,
 
549
            rqst.get('direction'), generate_merge_revisions=True,
 
550
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
475
551
        if not isinstance(view_revisions, list):
476
552
            view_revisions = list(view_revisions)
477
553
        view_revisions = _filter_revisions_touching_file_id(self.branch,
478
554
            rqst.get('specific_fileids')[0], view_revisions,
479
555
            include_merges=rqst.get('levels') != 1)
480
556
        return make_log_rev_iterator(self.branch, view_revisions,
481
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
557
            rqst.get('delta_type'), rqst.get('match'))
482
558
 
483
559
 
484
560
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
485
 
    generate_merge_revisions, delayed_graph_generation=False):
 
561
                         generate_merge_revisions,
 
562
                         delayed_graph_generation=False,
 
563
                         exclude_common_ancestry=False,
 
564
                         ):
486
565
    """Calculate the revisions to view.
487
566
 
488
567
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
489
568
             a list of the same tuples.
490
569
    """
 
570
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
 
571
        raise errors.BzrCommandError(gettext(
 
572
            '--exclude-common-ancestry requires two different revisions'))
 
573
    if direction not in ('reverse', 'forward'):
 
574
        raise ValueError(gettext('invalid direction %r') % direction)
491
575
    br_revno, br_rev_id = branch.last_revision_info()
492
576
    if br_revno == 0:
493
577
        return []
494
578
 
495
 
    # If a single revision is requested, check we can handle it
496
 
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
497
 
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
498
 
    if generate_single_revision:
499
 
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
500
 
 
501
 
    # If we only want to see linear revisions, we can iterate ...
 
579
    if (end_rev_id and start_rev_id == end_rev_id
 
580
        and (not generate_merge_revisions
 
581
             or not _has_merges(branch, end_rev_id))):
 
582
        # If a single revision is requested, check we can handle it
 
583
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
584
                                       br_revno)
502
585
    if not generate_merge_revisions:
503
 
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
504
 
            direction)
505
 
    else:
506
 
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
507
 
            direction, delayed_graph_generation)
 
586
        try:
 
587
            # If we only want to see linear revisions, we can iterate ...
 
588
            iter_revs = _linear_view_revisions(
 
589
                branch, start_rev_id, end_rev_id,
 
590
                exclude_common_ancestry=exclude_common_ancestry)
 
591
            # If a start limit was given and it's not obviously an
 
592
            # ancestor of the end limit, check it before outputting anything
 
593
            if (direction == 'forward'
 
594
                or (start_rev_id and not _is_obvious_ancestor(
 
595
                        branch, start_rev_id, end_rev_id))):
 
596
                    iter_revs = list(iter_revs)
 
597
            if direction == 'forward':
 
598
                iter_revs = reversed(iter_revs)
 
599
            return iter_revs
 
600
        except _StartNotLinearAncestor:
 
601
            # Switch to the slower implementation that may be able to find a
 
602
            # non-obvious ancestor out of the left-hand history.
 
603
            pass
 
604
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
605
                                        direction, delayed_graph_generation,
 
606
                                        exclude_common_ancestry)
 
607
    if direction == 'forward':
 
608
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
609
    return iter_revs
508
610
 
509
611
 
510
612
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
512
614
        # It's the tip
513
615
        return [(br_rev_id, br_revno, 0)]
514
616
    else:
515
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
516
 
        revno_str = '.'.join(str(n) for n in revno)
 
617
        revno_str = _compute_revno_str(branch, rev_id)
517
618
        return [(rev_id, revno_str, 0)]
518
619
 
519
620
 
520
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
521
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
522
 
    # If a start limit was given and it's not obviously an
523
 
    # ancestor of the end limit, check it before outputting anything
524
 
    if direction == 'forward' or (start_rev_id
525
 
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
526
 
        try:
527
 
            result = list(result)
528
 
        except _StartNotLinearAncestor:
529
 
            raise errors.BzrCommandError('Start revision not found in'
530
 
                ' left-hand history of end revision.')
531
 
    if direction == 'forward':
532
 
        result = reversed(result)
533
 
    return result
534
 
 
535
 
 
536
621
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
537
 
                            delayed_graph_generation):
 
622
                            delayed_graph_generation,
 
623
                            exclude_common_ancestry=False):
538
624
    # On large trees, generating the merge graph can take 30-60 seconds
539
625
    # so we delay doing it until a merge is detected, incrementally
540
626
    # returning initial (non-merge) revisions while we can.
546
632
    if delayed_graph_generation:
547
633
        try:
548
634
            for rev_id, revno, depth in  _linear_view_revisions(
549
 
                branch, start_rev_id, end_rev_id):
 
635
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
550
636
                if _has_merges(branch, rev_id):
551
637
                    # The end_rev_id can be nested down somewhere. We need an
552
638
                    # explicit ancestry check. There is an ambiguity here as we
553
639
                    # may not raise _StartNotLinearAncestor for a revision that
554
640
                    # is an ancestor but not a *linear* one. But since we have
555
641
                    # loaded the graph to do the check (or calculate a dotted
556
 
                    # revno), we may as well accept to show the log... 
557
 
                    # -- vila 100201
 
642
                    # revno), we may as well accept to show the log...  We need
 
643
                    # the check only if start_rev_id is not None as all
 
644
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
 
645
                    # -- vila 20100319
558
646
                    graph = branch.repository.get_graph()
559
 
                    if not graph.is_ancestor(start_rev_id, end_rev_id):
 
647
                    if (start_rev_id is not None
 
648
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
560
649
                        raise _StartNotLinearAncestor()
 
650
                    # Since we collected the revisions so far, we need to
 
651
                    # adjust end_rev_id.
561
652
                    end_rev_id = rev_id
562
653
                    break
563
654
                else:
564
655
                    initial_revisions.append((rev_id, revno, depth))
565
656
            else:
566
657
                # No merged revisions found
567
 
                if direction == 'reverse':
568
 
                    return initial_revisions
569
 
                elif direction == 'forward':
570
 
                    return reversed(initial_revisions)
571
 
                else:
572
 
                    raise ValueError('invalid direction %r' % direction)
 
658
                return initial_revisions
573
659
        except _StartNotLinearAncestor:
574
660
            # A merge was never detected so the lower revision limit can't
575
661
            # be nested down somewhere
576
 
            raise errors.BzrCommandError('Start revision not found in'
577
 
                ' history of end revision.')
 
662
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
663
                ' history of end revision.'))
 
664
 
 
665
    # We exit the loop above because we encounter a revision with merges, from
 
666
    # this revision, we need to switch to _graph_view_revisions.
578
667
 
579
668
    # A log including nested merges is required. If the direction is reverse,
580
669
    # we rebase the initial merge depths so that the development line is
583
672
    # indented at the end seems slightly nicer in that case.
584
673
    view_revisions = chain(iter(initial_revisions),
585
674
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
586
 
        rebase_initial_depths=direction == 'reverse'))
587
 
    if direction == 'reverse':
588
 
        return view_revisions
589
 
    elif direction == 'forward':
590
 
        # Forward means oldest first, adjusting for depth.
591
 
        view_revisions = reverse_by_depth(list(view_revisions))
592
 
        return _rebase_merge_depth(view_revisions)
593
 
    else:
594
 
        raise ValueError('invalid direction %r' % direction)
 
675
                              rebase_initial_depths=(direction == 'reverse'),
 
676
                              exclude_common_ancestry=exclude_common_ancestry))
 
677
    return view_revisions
595
678
 
596
679
 
597
680
def _has_merges(branch, rev_id):
600
683
    return len(parents) > 1
601
684
 
602
685
 
 
686
def _compute_revno_str(branch, rev_id):
 
687
    """Compute the revno string from a rev_id.
 
688
 
 
689
    :return: The revno string, or None if the revision is not in the supplied
 
690
        branch.
 
691
    """
 
692
    try:
 
693
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
694
    except errors.NoSuchRevision:
 
695
        # The revision must be outside of this branch
 
696
        return None
 
697
    else:
 
698
        return '.'.join(str(n) for n in revno)
 
699
 
 
700
 
603
701
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
604
702
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
605
703
    if start_rev_id and end_rev_id:
606
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
607
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
704
        try:
 
705
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
706
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
707
        except errors.NoSuchRevision:
 
708
            # one or both is not in the branch; not obvious
 
709
            return False
608
710
        if len(start_dotted) == 1 and len(end_dotted) == 1:
609
711
            # both on mainline
610
712
            return start_dotted[0] <= end_dotted[0]
620
722
    return True
621
723
 
622
724
 
623
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
725
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
726
                           exclude_common_ancestry=False):
624
727
    """Calculate a sequence of revisions to view, newest to oldest.
625
728
 
626
729
    :param start_rev_id: the lower revision-id
627
730
    :param end_rev_id: the upper revision-id
 
731
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
732
        the iterated revisions.
628
733
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
629
734
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
630
 
      is not found walking the left-hand history
 
735
        is not found walking the left-hand history
631
736
    """
632
737
    br_revno, br_rev_id = branch.last_revision_info()
633
738
    repo = branch.repository
 
739
    graph = repo.get_graph()
634
740
    if start_rev_id is None and end_rev_id is None:
635
741
        cur_revno = br_revno
636
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
742
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
743
            (_mod_revision.NULL_REVISION,)):
637
744
            yield revision_id, str(cur_revno), 0
638
745
            cur_revno -= 1
639
746
    else:
640
747
        if end_rev_id is None:
641
748
            end_rev_id = br_rev_id
642
749
        found_start = start_rev_id is None
643
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
644
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
645
 
            revno_str = '.'.join(str(n) for n in revno)
 
750
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
751
                (_mod_revision.NULL_REVISION,)):
 
752
            revno_str = _compute_revno_str(branch, revision_id)
646
753
            if not found_start and revision_id == start_rev_id:
647
 
                yield revision_id, revno_str, 0
 
754
                if not exclude_common_ancestry:
 
755
                    yield revision_id, revno_str, 0
648
756
                found_start = True
649
757
                break
650
758
            else:
655
763
 
656
764
 
657
765
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
658
 
    rebase_initial_depths=True):
 
766
                          rebase_initial_depths=True,
 
767
                          exclude_common_ancestry=False):
659
768
    """Calculate revisions to view including merges, newest to oldest.
660
769
 
661
770
    :param branch: the branch
665
774
      revision is found?
666
775
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
667
776
    """
 
777
    if exclude_common_ancestry:
 
778
        stop_rule = 'with-merges-without-common-ancestry'
 
779
    else:
 
780
        stop_rule = 'with-merges'
668
781
    view_revisions = branch.iter_merge_sorted_revisions(
669
782
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
670
 
        stop_rule="with-merges")
 
783
        stop_rule=stop_rule)
671
784
    if not rebase_initial_depths:
672
785
        for (rev_id, merge_depth, revno, end_of_merge
673
786
             ) in view_revisions:
693
806
            yield rev_id, '.'.join(map(str, revno)), merge_depth
694
807
 
695
808
 
696
 
@deprecated_function(deprecated_in((2, 2, 0)))
697
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
698
 
        specific_fileid, generate_merge_revisions):
699
 
    """Calculate the revisions to view.
700
 
 
701
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
702
 
             a list of the same tuples.
703
 
    """
704
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
705
 
        end_revision)
706
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
707
 
        direction, generate_merge_revisions or specific_fileid))
708
 
    if specific_fileid:
709
 
        view_revisions = _filter_revisions_touching_file_id(branch,
710
 
            specific_fileid, view_revisions,
711
 
            include_merges=generate_merge_revisions)
712
 
    return _rebase_merge_depth(view_revisions)
713
 
 
714
 
 
715
809
def _rebase_merge_depth(view_revisions):
716
810
    """Adjust depths upwards so the top level is 0."""
717
811
    # If either the first or last revision have a merge_depth of 0, we're done
761
855
    return log_rev_iterator
762
856
 
763
857
 
764
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
858
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
765
859
    """Create a filtered iterator of log_rev_iterator matching on a regex.
766
860
 
767
861
    :param branch: The branch being logged.
768
862
    :param generate_delta: Whether to generate a delta for each revision.
769
 
    :param search: A user text search string.
 
863
    :param match: A dictionary with properties as keys and lists of strings
 
864
        as values. To match, a revision may match any of the supplied strings
 
865
        within a single property but must match at least one string for each
 
866
        property.
770
867
    :param log_rev_iterator: An input iterator containing all revisions that
771
868
        could be displayed, in lists.
772
869
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
773
870
        delta).
774
871
    """
775
 
    if search is None:
 
872
    if match is None:
776
873
        return log_rev_iterator
777
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
778
 
            'log message filter')
779
 
    return _filter_message_re(searchRE, log_rev_iterator)
780
 
 
781
 
 
782
 
def _filter_message_re(searchRE, log_rev_iterator):
 
874
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
875
                for (k,v) in match.iteritems()]
 
876
    return _filter_re(searchRE, log_rev_iterator)
 
877
 
 
878
 
 
879
def _filter_re(searchRE, log_rev_iterator):
783
880
    for revs in log_rev_iterator:
784
 
        new_revs = []
785
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
786
 
            if searchRE.search(rev.message):
787
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
788
 
        yield new_revs
789
 
 
 
881
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
882
        if new_revs:
 
883
            yield new_revs
 
884
 
 
885
def _match_filter(searchRE, rev):
 
886
    strings = {
 
887
               'message': (rev.message,),
 
888
               'committer': (rev.committer,),
 
889
               'author': (rev.get_apparent_authors()),
 
890
               'bugs': list(rev.iter_bugs())
 
891
               }
 
892
    strings[''] = [item for inner_list in strings.itervalues()
 
893
                   for item in inner_list]
 
894
    for (k,v) in searchRE:
 
895
        if k in strings and not _match_any_filter(strings[k], v):
 
896
            return False
 
897
    return True
 
898
 
 
899
def _match_any_filter(strings, res):
 
900
    return any([filter(None, map(re.search, strings)) for re in res])
790
901
 
791
902
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
792
903
    fileids=None, direction='reverse'):
865
976
 
866
977
def _update_fileids(delta, fileids, stop_on):
867
978
    """Update the set of file-ids to search based on file lifecycle events.
868
 
    
 
979
 
869
980
    :param fileids: a set of fileids to update
870
981
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
871
982
      fileids set once their add or remove entry is detected respectively
912
1023
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
913
1024
        delta).
914
1025
    """
915
 
    repository = branch.repository
916
1026
    num = 9
917
1027
    for batch in log_rev_iterator:
918
1028
        batch = iter(batch)
967
1077
    if branch_revno != 0:
968
1078
        if (start_rev_id == _mod_revision.NULL_REVISION
969
1079
            or end_rev_id == _mod_revision.NULL_REVISION):
970
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1080
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
971
1081
        if start_revno > end_revno:
972
 
            raise errors.BzrCommandError("Start revision must be older than "
973
 
                                         "the end revision.")
 
1082
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1083
                                         "older than the end revision."))
974
1084
    return (start_rev_id, end_rev_id)
975
1085
 
976
1086
 
1025
1135
 
1026
1136
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1027
1137
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1028
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1138
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1029
1139
    if start_revno > end_revno:
1030
 
        raise errors.BzrCommandError("Start revision must be older than "
1031
 
                                     "the end revision.")
 
1140
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1141
                                     "than the end revision."))
1032
1142
 
1033
1143
    if end_revno < start_revno:
1034
1144
        return None, None, None, None
1035
1145
    cur_revno = branch_revno
1036
1146
    rev_nos = {}
1037
1147
    mainline_revs = []
1038
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1039
 
                        branch_last_revision):
 
1148
    graph = branch.repository.get_graph()
 
1149
    for revision_id in graph.iter_lefthand_ancestry(
 
1150
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1040
1151
        if cur_revno < start_revno:
1041
1152
            # We have gone far enough, but we always add 1 more revision
1042
1153
            rev_nos[revision_id] = cur_revno
1056
1167
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1057
1168
 
1058
1169
 
1059
 
@deprecated_function(deprecated_in((2, 2, 0)))
1060
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1061
 
    """Filter view_revisions based on revision ranges.
1062
 
 
1063
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1064
 
            tuples to be filtered.
1065
 
 
1066
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1067
 
            If NONE then all revisions up to the end_rev_id are logged.
1068
 
 
1069
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1070
 
            If NONE then all revisions up to the end of the log are logged.
1071
 
 
1072
 
    :return: The filtered view_revisions.
1073
 
    """
1074
 
    if start_rev_id or end_rev_id:
1075
 
        revision_ids = [r for r, n, d in view_revisions]
1076
 
        if start_rev_id:
1077
 
            start_index = revision_ids.index(start_rev_id)
1078
 
        else:
1079
 
            start_index = 0
1080
 
        if start_rev_id == end_rev_id:
1081
 
            end_index = start_index
1082
 
        else:
1083
 
            if end_rev_id:
1084
 
                end_index = revision_ids.index(end_rev_id)
1085
 
            else:
1086
 
                end_index = len(view_revisions) - 1
1087
 
        # To include the revisions merged into the last revision,
1088
 
        # extend end_rev_id down to, but not including, the next rev
1089
 
        # with the same or lesser merge_depth
1090
 
        end_merge_depth = view_revisions[end_index][2]
1091
 
        try:
1092
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1093
 
                if view_revisions[index][2] <= end_merge_depth:
1094
 
                    end_index = index - 1
1095
 
                    break
1096
 
        except IndexError:
1097
 
            # if the search falls off the end then log to the end as well
1098
 
            end_index = len(view_revisions) - 1
1099
 
        view_revisions = view_revisions[start_index:end_index+1]
1100
 
    return view_revisions
1101
 
 
1102
 
 
1103
1170
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1104
1171
    include_merges=True):
1105
1172
    r"""Return the list of revision ids which touch a given file id.
1108
1175
    This includes the revisions which directly change the file id,
1109
1176
    and the revisions which merge these changes. So if the
1110
1177
    revision graph is::
 
1178
 
1111
1179
        A-.
1112
1180
        |\ \
1113
1181
        B C E
1140
1208
    """
1141
1209
    # Lookup all possible text keys to determine which ones actually modified
1142
1210
    # the file.
 
1211
    graph = branch.repository.get_file_graph()
 
1212
    get_parent_map = graph.get_parent_map
1143
1213
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1144
1214
    next_keys = None
1145
1215
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1149
1219
    #       indexing layer. We might consider passing in hints as to the known
1150
1220
    #       access pattern (sparse/clustered, high success rate/low success
1151
1221
    #       rate). This particular access is clustered with a low success rate.
1152
 
    get_parent_map = branch.repository.texts.get_parent_map
1153
1222
    modified_text_revisions = set()
1154
1223
    chunk_size = 1000
1155
1224
    for start in xrange(0, len(text_keys), chunk_size):
1182
1251
    return result
1183
1252
 
1184
1253
 
1185
 
@deprecated_function(deprecated_in((2, 2, 0)))
1186
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1187
 
                       include_merges=True):
1188
 
    """Produce an iterator of revisions to show
1189
 
    :return: an iterator of (revision_id, revno, merge_depth)
1190
 
    (if there is no revno for a revision, None is supplied)
1191
 
    """
1192
 
    if not include_merges:
1193
 
        revision_ids = mainline_revs[1:]
1194
 
        if direction == 'reverse':
1195
 
            revision_ids.reverse()
1196
 
        for revision_id in revision_ids:
1197
 
            yield revision_id, str(rev_nos[revision_id]), 0
1198
 
        return
1199
 
    graph = branch.repository.get_graph()
1200
 
    # This asks for all mainline revisions, which means we only have to spider
1201
 
    # sideways, rather than depth history. That said, its still size-of-history
1202
 
    # and should be addressed.
1203
 
    # mainline_revisions always includes an extra revision at the beginning, so
1204
 
    # don't request it.
1205
 
    parent_map = dict(((key, value) for key, value in
1206
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1207
 
    # filter out ghosts; merge_sort errors on ghosts.
1208
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1209
 
    merge_sorted_revisions = tsort.merge_sort(
1210
 
        rev_graph,
1211
 
        mainline_revs[-1],
1212
 
        mainline_revs,
1213
 
        generate_revno=True)
1214
 
 
1215
 
    if direction == 'forward':
1216
 
        # forward means oldest first.
1217
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1218
 
    elif direction != 'reverse':
1219
 
        raise ValueError('invalid direction %r' % direction)
1220
 
 
1221
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1222
 
         ) in merge_sorted_revisions:
1223
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1224
 
 
1225
 
 
1226
1254
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1227
1255
    """Reverse revisions by depth.
1228
1256
 
1263
1291
    """
1264
1292
 
1265
1293
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1266
 
                 tags=None, diff=None):
 
1294
                 tags=None, diff=None, signature=None):
1267
1295
        self.rev = rev
1268
 
        self.revno = str(revno)
 
1296
        if revno is None:
 
1297
            self.revno = None
 
1298
        else:
 
1299
            self.revno = str(revno)
1269
1300
        self.merge_depth = merge_depth
1270
1301
        self.delta = delta
1271
1302
        self.tags = tags
1272
1303
        self.diff = diff
 
1304
        self.signature = signature
1273
1305
 
1274
1306
 
1275
1307
class LogFormatter(object):
1284
1316
    to indicate which LogRevision attributes it supports:
1285
1317
 
1286
1318
    - supports_delta must be True if this log formatter supports delta.
1287
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1288
 
        attribute describes whether the 'short_status' format (1) or the long
1289
 
        one (2) should be used.
 
1319
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1320
      attribute describes whether the 'short_status' format (1) or the long
 
1321
      one (2) should be used.
1290
1322
 
1291
1323
    - supports_merge_revisions must be True if this log formatter supports
1292
 
        merge revisions.  If not, then only mainline revisions will be passed
1293
 
        to the formatter.
 
1324
      merge revisions.  If not, then only mainline revisions will be passed
 
1325
      to the formatter.
1294
1326
 
1295
1327
    - preferred_levels is the number of levels this formatter defaults to.
1296
 
        The default value is zero meaning display all levels.
1297
 
        This value is only relevant if supports_merge_revisions is True.
 
1328
      The default value is zero meaning display all levels.
 
1329
      This value is only relevant if supports_merge_revisions is True.
1298
1330
 
1299
1331
    - supports_tags must be True if this log formatter supports tags.
1300
 
        Otherwise the tags attribute may not be populated.
 
1332
      Otherwise the tags attribute may not be populated.
1301
1333
 
1302
1334
    - supports_diff must be True if this log formatter supports diffs.
1303
 
        Otherwise the diff attribute may not be populated.
 
1335
      Otherwise the diff attribute may not be populated.
 
1336
 
 
1337
    - supports_signatures must be True if this log formatter supports GPG
 
1338
      signatures.
1304
1339
 
1305
1340
    Plugins can register functions to show custom revision properties using
1306
1341
    the properties_handler_registry. The registered function
1307
 
    must respect the following interface description:
 
1342
    must respect the following interface description::
 
1343
 
1308
1344
        def my_show_properties(properties_dict):
1309
1345
            # code that returns a dict {'name':'value'} of the properties
1310
1346
            # to be shown
1313
1349
 
1314
1350
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1315
1351
                 delta_format=None, levels=None, show_advice=False,
1316
 
                 to_exact_file=None):
 
1352
                 to_exact_file=None, author_list_handler=None):
1317
1353
        """Create a LogFormatter.
1318
1354
 
1319
1355
        :param to_file: the file to output to
1320
 
        :param to_exact_file: if set, gives an output stream to which 
 
1356
        :param to_exact_file: if set, gives an output stream to which
1321
1357
             non-Unicode diffs are written.
1322
1358
        :param show_ids: if True, revision-ids are to be displayed
1323
1359
        :param show_timezone: the timezone to use
1327
1363
          let the log formatter decide.
1328
1364
        :param show_advice: whether to show advice at the end of the
1329
1365
          log or not
 
1366
        :param author_list_handler: callable generating a list of
 
1367
          authors to display for a given revision
1330
1368
        """
1331
1369
        self.to_file = to_file
1332
1370
        # 'exact' stream used to show diff, it should print content 'as is'
1347
1385
        self.levels = levels
1348
1386
        self._show_advice = show_advice
1349
1387
        self._merge_count = 0
 
1388
        self._author_list_handler = author_list_handler
1350
1389
 
1351
1390
    def get_levels(self):
1352
1391
        """Get the number of levels to display or 0 for all."""
1371
1410
            if advice_sep:
1372
1411
                self.to_file.write(advice_sep)
1373
1412
            self.to_file.write(
1374
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1413
                "Use --include-merged or -n0 to see merged revisions.\n")
1375
1414
 
1376
1415
    def get_advice_separator(self):
1377
1416
        """Get the text separating the log from the closing advice."""
1384
1423
        return address
1385
1424
 
1386
1425
    def short_author(self, rev):
1387
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1388
 
        if name:
1389
 
            return name
1390
 
        return address
 
1426
        return self.authors(rev, 'first', short=True, sep=', ')
 
1427
 
 
1428
    def authors(self, rev, who, short=False, sep=None):
 
1429
        """Generate list of authors, taking --authors option into account.
 
1430
 
 
1431
        The caller has to specify the name of a author list handler,
 
1432
        as provided by the author list registry, using the ``who``
 
1433
        argument.  That name only sets a default, though: when the
 
1434
        user selected a different author list generation using the
 
1435
        ``--authors`` command line switch, as represented by the
 
1436
        ``author_list_handler`` constructor argument, that value takes
 
1437
        precedence.
 
1438
 
 
1439
        :param rev: The revision for which to generate the list of authors.
 
1440
        :param who: Name of the default handler.
 
1441
        :param short: Whether to shorten names to either name or address.
 
1442
        :param sep: What separator to use for automatic concatenation.
 
1443
        """
 
1444
        if self._author_list_handler is not None:
 
1445
            # The user did specify --authors, which overrides the default
 
1446
            author_list_handler = self._author_list_handler
 
1447
        else:
 
1448
            # The user didn't specify --authors, so we use the caller's default
 
1449
            author_list_handler = author_list_registry.get(who)
 
1450
        names = author_list_handler(rev)
 
1451
        if short:
 
1452
            for i in range(len(names)):
 
1453
                name, address = config.parse_username(names[i])
 
1454
                if name:
 
1455
                    names[i] = name
 
1456
                else:
 
1457
                    names[i] = address
 
1458
        if sep is not None:
 
1459
            names = sep.join(names)
 
1460
        return names
1391
1461
 
1392
1462
    def merge_marker(self, revision):
1393
1463
        """Get the merge marker to include in the output or '' if none."""
1424
1494
        """
1425
1495
        # Revision comes directly from a foreign repository
1426
1496
        if isinstance(rev, foreign.ForeignRevision):
1427
 
            return self._format_properties(rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1497
            return self._format_properties(
 
1498
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1428
1499
 
1429
1500
        # Imported foreign revision revision ids always contain :
1430
1501
        if not ":" in rev.revision_id:
1462
1533
    supports_delta = True
1463
1534
    supports_tags = True
1464
1535
    supports_diff = True
 
1536
    supports_signatures = True
1465
1537
 
1466
1538
    def __init__(self, *args, **kwargs):
1467
1539
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1487
1559
                self.merge_marker(revision)))
1488
1560
        if revision.tags:
1489
1561
            lines.append('tags: %s' % (', '.join(revision.tags)))
1490
 
        if self.show_ids:
 
1562
        if self.show_ids or revision.revno is None:
1491
1563
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1564
        if self.show_ids:
1492
1565
            for parent_id in revision.rev.parent_ids:
1493
1566
                lines.append('parent: %s' % (parent_id,))
1494
1567
        lines.extend(self.custom_properties(revision.rev))
1495
1568
 
1496
1569
        committer = revision.rev.committer
1497
 
        authors = revision.rev.get_apparent_authors()
 
1570
        authors = self.authors(revision.rev, 'all')
1498
1571
        if authors != [committer]:
1499
1572
            lines.append('author: %s' % (", ".join(authors),))
1500
1573
        lines.append('committer: %s' % (committer,))
1505
1578
 
1506
1579
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1507
1580
 
 
1581
        if revision.signature is not None:
 
1582
            lines.append('signature: ' + revision.signature)
 
1583
 
1508
1584
        lines.append('message:')
1509
1585
        if not revision.rev.message:
1510
1586
            lines.append('  (no message)')
1517
1593
        to_file = self.to_file
1518
1594
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1519
1595
        if revision.delta is not None:
1520
 
            # We don't respect delta_format for compatibility
1521
 
            revision.delta.show(to_file, self.show_ids, indent=indent,
1522
 
                                short_status=False)
 
1596
            # Use the standard status output to display changes
 
1597
            from bzrlib.delta import report_delta
 
1598
            report_delta(to_file, revision.delta, short_status=False,
 
1599
                         show_ids=self.show_ids, indent=indent)
1523
1600
        if revision.diff is not None:
1524
1601
            to_file.write(indent + 'diff:\n')
1525
1602
            to_file.flush()
1556
1633
        indent = '    ' * depth
1557
1634
        revno_width = self.revno_width_by_depth.get(depth)
1558
1635
        if revno_width is None:
1559
 
            if revision.revno.find('.') == -1:
 
1636
            if revision.revno is None or revision.revno.find('.') == -1:
1560
1637
                # mainline revno, e.g. 12345
1561
1638
                revno_width = 5
1562
1639
            else:
1570
1647
        if revision.tags:
1571
1648
            tags = ' {%s}' % (', '.join(revision.tags))
1572
1649
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1573
 
                revision.revno, self.short_author(revision.rev),
 
1650
                revision.revno or "", self.short_author(revision.rev),
1574
1651
                format_date(revision.rev.timestamp,
1575
1652
                            revision.rev.timezone or 0,
1576
1653
                            self.show_timezone, date_fmt="%Y-%m-%d",
1577
1654
                            show_offset=False),
1578
1655
                tags, self.merge_marker(revision)))
1579
1656
        self.show_properties(revision.rev, indent+offset)
1580
 
        if self.show_ids:
 
1657
        if self.show_ids or revision.revno is None:
1581
1658
            to_file.write(indent + offset + 'revision-id:%s\n'
1582
1659
                          % (revision.rev.revision_id,))
1583
1660
        if not revision.rev.message:
1588
1665
                to_file.write(indent + offset + '%s\n' % (l,))
1589
1666
 
1590
1667
        if revision.delta is not None:
1591
 
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
1592
 
                                short_status=self.delta_format==1)
 
1668
            # Use the standard status output to display changes
 
1669
            from bzrlib.delta import report_delta
 
1670
            report_delta(to_file, revision.delta,
 
1671
                         short_status=self.delta_format==1,
 
1672
                         show_ids=self.show_ids, indent=indent + offset)
1593
1673
        if revision.diff is not None:
1594
1674
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1595
1675
        to_file.write('\n')
1633
1713
 
1634
1714
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1635
1715
        """Format log info into one string. Truncate tail of string
1636
 
        :param  revno:      revision number or None.
1637
 
                            Revision numbers counts from 1.
1638
 
        :param  rev:        revision object
1639
 
        :param  max_chars:  maximum length of resulting string
1640
 
        :param  tags:       list of tags or None
1641
 
        :param  prefix:     string to prefix each line
1642
 
        :return:            formatted truncated string
 
1716
 
 
1717
        :param revno:      revision number or None.
 
1718
                           Revision numbers counts from 1.
 
1719
        :param rev:        revision object
 
1720
        :param max_chars:  maximum length of resulting string
 
1721
        :param tags:       list of tags or None
 
1722
        :param prefix:     string to prefix each line
 
1723
        :return:           formatted truncated string
1643
1724
        """
1644
1725
        out = []
1645
1726
        if revno:
1646
1727
            # show revno only when is not None
1647
1728
            out.append("%s:" % revno)
1648
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1729
        if max_chars is not None:
 
1730
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1731
        else:
 
1732
            out.append(self.short_author(rev))
1649
1733
        out.append(self.date_string(rev))
1650
1734
        if len(rev.parent_ids) > 1:
1651
1735
            out.append('[merge]')
1670
1754
                               self.show_timezone,
1671
1755
                               date_fmt='%Y-%m-%d',
1672
1756
                               show_offset=False)
1673
 
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1757
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1758
        committer_str = committer_str.replace(' <', '  <')
1674
1759
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1675
1760
 
1676
1761
        if revision.delta is not None and revision.delta.has_changed():
1709
1794
        return self.get(name)(*args, **kwargs)
1710
1795
 
1711
1796
    def get_default(self, branch):
1712
 
        return self.get(branch.get_config().log_format())
 
1797
        c = branch.get_config_stack()
 
1798
        return self.get(c.get('log_format'))
1713
1799
 
1714
1800
 
1715
1801
log_formatter_registry = LogFormatterRegistry()
1716
1802
 
1717
1803
 
1718
1804
log_formatter_registry.register('short', ShortLogFormatter,
1719
 
                                'Moderately short log format')
 
1805
                                'Moderately short log format.')
1720
1806
log_formatter_registry.register('long', LongLogFormatter,
1721
 
                                'Detailed log format')
 
1807
                                'Detailed log format.')
1722
1808
log_formatter_registry.register('line', LineLogFormatter,
1723
 
                                'Log format with one line per revision')
 
1809
                                'Log format with one line per revision.')
1724
1810
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1725
 
                                'Format used by GNU ChangeLog files')
 
1811
                                'Format used by GNU ChangeLog files.')
1726
1812
 
1727
1813
 
1728
1814
def register_formatter(name, formatter):
1738
1824
    try:
1739
1825
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1740
1826
    except KeyError:
1741
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1742
 
 
1743
 
 
1744
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1745
 
    # deprecated; for compatibility
1746
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1747
 
    lf.show(revno, rev, delta)
 
1827
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1828
 
 
1829
 
 
1830
def author_list_all(rev):
 
1831
    return rev.get_apparent_authors()[:]
 
1832
 
 
1833
 
 
1834
def author_list_first(rev):
 
1835
    lst = rev.get_apparent_authors()
 
1836
    try:
 
1837
        return [lst[0]]
 
1838
    except IndexError:
 
1839
        return []
 
1840
 
 
1841
 
 
1842
def author_list_committer(rev):
 
1843
    return [rev.committer]
 
1844
 
 
1845
 
 
1846
author_list_registry = registry.Registry()
 
1847
 
 
1848
author_list_registry.register('all', author_list_all,
 
1849
                              'All authors')
 
1850
 
 
1851
author_list_registry.register('first', author_list_first,
 
1852
                              'The first author')
 
1853
 
 
1854
author_list_registry.register('committer', author_list_committer,
 
1855
                              'The committer')
1748
1856
 
1749
1857
 
1750
1858
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1815
1923
    old_revisions = set()
1816
1924
    new_history = []
1817
1925
    new_revisions = set()
1818
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1819
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1926
    graph = repository.get_graph()
 
1927
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1928
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1820
1929
    stop_revision = None
1821
1930
    do_old = True
1822
1931
    do_new = True
1897
2006
        lf.log_revision(lr)
1898
2007
 
1899
2008
 
1900
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2009
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1901
2010
    """Find file-ids and kinds given a list of files and a revision range.
1902
2011
 
1903
2012
    We search for files at the end of the range. If not found there,
1907
2016
    :param file_list: the list of paths given on the command line;
1908
2017
      the first of these can be a branch location or a file path,
1909
2018
      the remainder must be file paths
 
2019
    :param add_cleanup: When the branch returned is read locked,
 
2020
      an unlock call will be queued to the cleanup.
1910
2021
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1911
2022
      info_list is a list of (relative_path, file_id, kind) tuples where
1912
2023
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1913
2024
      branch will be read-locked.
1914
2025
    """
1915
 
    from builtins import _get_revision_range, safe_relpath_files
1916
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1917
 
    b.lock_read()
 
2026
    from bzrlib.builtins import _get_revision_range
 
2027
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2028
        file_list[0])
 
2029
    add_cleanup(b.lock_read().unlock)
1918
2030
    # XXX: It's damn messy converting a list of paths to relative paths when
1919
2031
    # those paths might be deleted ones, they might be on a case-insensitive
1920
2032
    # filesystem and/or they might be in silly locations (like another branch).
1924
2036
    # case of running log in a nested directory, assuming paths beyond the
1925
2037
    # first one haven't been deleted ...
1926
2038
    if tree:
1927
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2039
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1928
2040
    else:
1929
2041
        relpaths = [path] + file_list[1:]
1930
2042
    info_list = []
2006
2118
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2007
2119
        fixed_bug_urls = [row[0] for row in bug_rows if
2008
2120
                          len(row) > 1 and row[1] == 'fixed']
2009
 
        
 
2121
 
2010
2122
        if fixed_bug_urls:
2011
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2123
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2124
                    ' '.join(fixed_bug_urls)}
2012
2125
    return {}
2013
2126
 
2014
2127
properties_handler_registry.register('bugs_properties_handler',