~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-03-25 00:02:51 UTC
  • mfrom: (5106.1.1 version-bump)
  • Revision ID: pqm@pqm.ubuntu.com-20100325000251-bwsv5c5d3l9x3lnn
(Jelmer) Bump API version for 2.2.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2010 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
 
17
19
"""Code to show logs of changes.
18
20
 
19
21
Various flavors of log can be produced:
47
49
all the changes since the previous revision that touched hello.c.
48
50
"""
49
51
 
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,
68
69
    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,
76
77
    tsort,
77
78
    )
78
 
from bzrlib.i18n import gettext, ngettext
79
79
""")
80
80
 
81
81
from bzrlib import (
82
 
    lazy_regex,
83
82
    registry,
84
83
    )
85
84
from bzrlib.osutils import (
86
85
    format_date,
87
86
    format_date_with_offset_in_original_timezone,
88
 
    get_diff_header_encoding,
89
87
    get_terminal_encoding,
 
88
    re_compile_checked,
90
89
    terminal_width,
91
90
    )
 
91
from bzrlib.symbol_versioning import (
 
92
    deprecated_function,
 
93
    deprecated_in,
 
94
    )
92
95
 
93
96
 
94
97
def find_touching_revisions(branch, file_id):
105
108
    last_ie = None
106
109
    last_path = None
107
110
    revno = 1
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):
 
111
    for revision_id in branch.revision_history():
112
112
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if this_inv.has_id(file_id):
 
113
        if file_id in this_inv:
114
114
            this_ie = this_inv[file_id]
115
115
            this_path = this_inv.id2path(file_id)
116
116
        else:
138
138
        revno += 1
139
139
 
140
140
 
 
141
def _enumerate_history(branch):
 
142
    rh = []
 
143
    revno = 1
 
144
    for rev_id in branch.revision_history():
 
145
        rh.append((revno, rev_id))
 
146
        revno += 1
 
147
    return rh
 
148
 
 
149
 
141
150
def show_log(branch,
142
151
             lf,
143
152
             specific_fileid=None,
147
156
             end_revision=None,
148
157
             search=None,
149
158
             limit=None,
150
 
             show_diff=False,
151
 
             match=None):
 
159
             show_diff=False):
152
160
    """Write out human-readable log of commits to this branch.
153
161
 
154
162
    This function is being retained for backwards compatibility but
177
185
        if None or 0.
178
186
 
179
187
    :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.
183
188
    """
184
189
    # Convert old-style parameters to new-style parameters
185
190
    if specific_fileid is not None:
209
214
    Logger(branch, rqst).show(lf)
210
215
 
211
216
 
212
 
# Note: This needs to be kept in sync with the defaults in
 
217
# Note: This needs to be kept this in sync with the defaults in
213
218
# make_log_request_dict() below
214
219
_DEFAULT_REQUEST_PARAMS = {
215
220
    'direction': 'reverse',
216
 
    'levels': None,
 
221
    'levels': 1,
217
222
    'generate_tags': True,
218
 
    'exclude_common_ancestry': False,
219
223
    '_match_using_deltas': True,
220
224
    }
221
225
 
222
226
 
223
227
def make_log_request_dict(direction='reverse', specific_fileids=None,
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
 
                          ):
 
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):
231
231
    """Convenience function for making a logging request dictionary.
232
232
 
233
233
    Using this function may make code slightly safer by ensuring
253
253
      matching commit messages
254
254
 
255
255
    :param levels: the number of levels of revisions to
256
 
      generate; 1 for just the mainline; 0 for all levels, or None for
257
 
      a sensible default.
 
256
      generate; 1 for just the mainline; 0 for all levels.
258
257
 
259
258
    :param generate_tags: If True, include tags for matched revisions.
260
 
`
 
259
 
261
260
    :param delta_type: Either 'full', 'partial' or None.
262
261
      'full' means generate the complete delta - adds/deletes/modifies/etc;
263
262
      'partial' means filter the delta using specific_fileids;
272
271
      algorithm used for matching specific_fileids. This parameter
273
272
      may be removed in the future so bzrlib client code should NOT
274
273
      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
 
 
288
274
    """
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] }
298
275
    return {
299
276
        'direction': direction,
300
277
        'specific_fileids': specific_fileids,
301
278
        'start_revision': start_revision,
302
279
        'end_revision': end_revision,
303
280
        'limit': limit,
 
281
        'message_search': message_search,
304
282
        'levels': levels,
305
283
        'generate_tags': generate_tags,
306
284
        'delta_type': delta_type,
307
285
        'diff_type': diff_type,
308
 
        'exclude_common_ancestry': exclude_common_ancestry,
309
 
        'signature': signature,
310
 
        'match': match,
311
 
        'omit_merges': omit_merges,
312
286
        # Add 'private' attributes for features that may be deprecated
313
287
        '_match_using_deltas': _match_using_deltas,
314
288
    }
316
290
 
317
291
def _apply_log_request_defaults(rqst):
318
292
    """Apply default values to a request dictionary."""
319
 
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
293
    result = _DEFAULT_REQUEST_PARAMS
320
294
    if rqst:
321
295
        result.update(rqst)
322
296
    return result
323
297
 
324
298
 
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
 
 
346
299
class LogGenerator(object):
347
300
    """A generator of log revisions."""
348
301
 
393
346
        # Tweak the LogRequest based on what the LogFormatter can handle.
394
347
        # (There's no point generating stuff if the formatter can't display it.)
395
348
        rqst = self.rqst
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
 
 
 
349
        rqst['levels'] = lf.get_levels()
400
350
        if not getattr(lf, 'supports_tags', False):
401
351
            rqst['generate_tags'] = False
402
352
        if not getattr(lf, 'supports_delta', False):
403
353
            rqst['delta_type'] = None
404
354
        if not getattr(lf, 'supports_diff', False):
405
355
            rqst['diff_type'] = None
406
 
        if not getattr(lf, 'supports_signatures', False):
407
 
            rqst['signature'] = False
408
356
 
409
357
        # Find and print the interesting revisions
410
358
        generator = self._generator_factory(self.branch, rqst)
414
362
 
415
363
    def _generator_factory(self, branch, rqst):
416
364
        """Make the LogGenerator object to use.
417
 
 
 
365
        
418
366
        Subclasses may wish to override this.
419
367
        """
420
368
        return _DefaultLogGenerator(branch, rqst)
444
392
        levels = rqst.get('levels')
445
393
        limit = rqst.get('limit')
446
394
        diff_type = rqst.get('diff_type')
447
 
        show_signature = rqst.get('signature')
448
 
        omit_merges = rqst.get('omit_merges')
449
395
        log_count = 0
450
396
        revision_iterator = self._create_log_revision_iterator()
451
397
        for revs in revision_iterator:
453
399
                # 0 levels means show everything; merge_depth counts from 0
454
400
                if levels != 0 and merge_depth >= levels:
455
401
                    continue
456
 
                if omit_merges and len(rev.parent_ids) > 1:
457
 
                    continue
458
402
                if diff_type is None:
459
403
                    diff = None
460
404
                else:
461
405
                    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
467
406
                yield LogRevision(rev, revno, merge_depth, delta,
468
 
                    self.rev_tag_dict.get(rev_id), diff, signature)
 
407
                    self.rev_tag_dict.get(rev_id), diff)
469
408
                if limit:
470
409
                    log_count += 1
471
410
                    if log_count >= limit:
485
424
        else:
486
425
            specific_files = None
487
426
        s = StringIO()
488
 
        path_encoding = get_diff_header_encoding()
489
427
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
490
 
            new_label='', path_encoding=path_encoding)
 
428
            new_label='')
491
429
        return s.getvalue()
492
430
 
493
431
    def _create_log_revision_iterator(self):
517
455
        generate_merge_revisions = rqst.get('levels') != 1
518
456
        delayed_graph_generation = not rqst.get('specific_fileids') and (
519
457
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
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'))
 
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)
526
461
 
527
462
        # Apply the other filters
528
463
        return make_log_rev_iterator(self.branch, view_revisions,
529
 
            rqst.get('delta_type'), rqst.get('match'),
 
464
            rqst.get('delta_type'), rqst.get('message_search'),
530
465
            file_ids=rqst.get('specific_fileids'),
531
466
            direction=rqst.get('direction'))
532
467
 
535
470
        # Note that we always generate the merge revisions because
536
471
        # filter_revisions_touching_file_id() requires them ...
537
472
        rqst = self.rqst
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'))
 
473
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
474
            self.end_rev_id, rqst.get('direction'), True)
542
475
        if not isinstance(view_revisions, list):
543
476
            view_revisions = list(view_revisions)
544
477
        view_revisions = _filter_revisions_touching_file_id(self.branch,
545
478
            rqst.get('specific_fileids')[0], view_revisions,
546
479
            include_merges=rqst.get('levels') != 1)
547
480
        return make_log_rev_iterator(self.branch, view_revisions,
548
 
            rqst.get('delta_type'), rqst.get('match'))
 
481
            rqst.get('delta_type'), rqst.get('message_search'))
549
482
 
550
483
 
551
484
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
552
 
                         generate_merge_revisions,
553
 
                         delayed_graph_generation=False,
554
 
                         exclude_common_ancestry=False,
555
 
                         ):
 
485
    generate_merge_revisions, delayed_graph_generation=False):
556
486
    """Calculate the revisions to view.
557
487
 
558
488
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
559
489
             a list of the same tuples.
560
490
    """
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)
566
491
    br_revno, br_rev_id = branch.last_revision_info()
567
492
    if br_revno == 0:
568
493
        return []
569
494
 
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)
 
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 ...
576
502
    if not generate_merge_revisions:
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
 
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)
601
508
 
602
509
 
603
510
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
605
512
        # It's the tip
606
513
        return [(br_rev_id, br_revno, 0)]
607
514
    else:
608
 
        revno_str = _compute_revno_str(branch, rev_id)
 
515
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
516
        revno_str = '.'.join(str(n) for n in revno)
609
517
        return [(rev_id, revno_str, 0)]
610
518
 
611
519
 
 
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
 
612
536
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
613
 
                            delayed_graph_generation,
614
 
                            exclude_common_ancestry=False):
 
537
                            delayed_graph_generation):
615
538
    # On large trees, generating the merge graph can take 30-60 seconds
616
539
    # so we delay doing it until a merge is detected, incrementally
617
540
    # returning initial (non-merge) revisions while we can.
623
546
    if delayed_graph_generation:
624
547
        try:
625
548
            for rev_id, revno, depth in  _linear_view_revisions(
626
 
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
549
                branch, start_rev_id, end_rev_id):
627
550
                if _has_merges(branch, rev_id):
628
551
                    # The end_rev_id can be nested down somewhere. We need an
629
552
                    # explicit ancestry check. There is an ambiguity here as we
646
569
                    initial_revisions.append((rev_id, revno, depth))
647
570
            else:
648
571
                # No merged revisions found
649
 
                return initial_revisions
 
572
                if direction == 'reverse':
 
573
                    return initial_revisions
 
574
                elif direction == 'forward':
 
575
                    return reversed(initial_revisions)
 
576
                else:
 
577
                    raise ValueError('invalid direction %r' % direction)
650
578
        except _StartNotLinearAncestor:
651
579
            # A merge was never detected so the lower revision limit can't
652
580
            # be nested down somewhere
653
 
            raise errors.BzrCommandError(gettext('Start revision not found in'
654
 
                ' history of end revision.'))
 
581
            raise errors.BzrCommandError('Start revision not found in'
 
582
                ' history of end revision.')
655
583
 
656
584
    # We exit the loop above because we encounter a revision with merges, from
657
585
    # this revision, we need to switch to _graph_view_revisions.
663
591
    # indented at the end seems slightly nicer in that case.
664
592
    view_revisions = chain(iter(initial_revisions),
665
593
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
666
 
                              rebase_initial_depths=(direction == 'reverse'),
667
 
                              exclude_common_ancestry=exclude_common_ancestry))
668
 
    return view_revisions
 
594
                              rebase_initial_depths=(direction == 'reverse')))
 
595
    if direction == 'reverse':
 
596
        return view_revisions
 
597
    elif direction == 'forward':
 
598
        # Forward means oldest first, adjusting for depth.
 
599
        view_revisions = reverse_by_depth(list(view_revisions))
 
600
        return _rebase_merge_depth(view_revisions)
 
601
    else:
 
602
        raise ValueError('invalid direction %r' % direction)
669
603
 
670
604
 
671
605
def _has_merges(branch, rev_id):
674
608
    return len(parents) > 1
675
609
 
676
610
 
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
 
 
692
611
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
693
612
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
694
613
    if start_rev_id and 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
 
614
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
615
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
701
616
        if len(start_dotted) == 1 and len(end_dotted) == 1:
702
617
            # both on mainline
703
618
            return start_dotted[0] <= end_dotted[0]
713
628
    return True
714
629
 
715
630
 
716
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
717
 
                           exclude_common_ancestry=False):
 
631
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
718
632
    """Calculate a sequence of revisions to view, newest to oldest.
719
633
 
720
634
    :param start_rev_id: the lower revision-id
721
635
    :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.
724
636
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
725
637
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
726
 
        is not found walking the left-hand history
 
638
      is not found walking the left-hand history
727
639
    """
728
640
    br_revno, br_rev_id = branch.last_revision_info()
729
641
    repo = branch.repository
730
 
    graph = repo.get_graph()
731
642
    if start_rev_id is None and end_rev_id is None:
732
643
        cur_revno = br_revno
733
 
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
734
 
            (_mod_revision.NULL_REVISION,)):
 
644
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
735
645
            yield revision_id, str(cur_revno), 0
736
646
            cur_revno -= 1
737
647
    else:
738
648
        if end_rev_id is None:
739
649
            end_rev_id = br_rev_id
740
650
        found_start = start_rev_id is None
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)
 
651
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
 
652
            revno = branch.revision_id_to_dotted_revno(revision_id)
 
653
            revno_str = '.'.join(str(n) for n in revno)
744
654
            if not found_start and revision_id == start_rev_id:
745
 
                if not exclude_common_ancestry:
746
 
                    yield revision_id, revno_str, 0
 
655
                yield revision_id, revno_str, 0
747
656
                found_start = True
748
657
                break
749
658
            else:
754
663
 
755
664
 
756
665
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
757
 
                          rebase_initial_depths=True,
758
 
                          exclude_common_ancestry=False):
 
666
                          rebase_initial_depths=True):
759
667
    """Calculate revisions to view including merges, newest to oldest.
760
668
 
761
669
    :param branch: the branch
765
673
      revision is found?
766
674
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
767
675
    """
768
 
    if exclude_common_ancestry:
769
 
        stop_rule = 'with-merges-without-common-ancestry'
770
 
    else:
771
 
        stop_rule = 'with-merges'
772
676
    view_revisions = branch.iter_merge_sorted_revisions(
773
677
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
774
 
        stop_rule=stop_rule)
 
678
        stop_rule="with-merges")
775
679
    if not rebase_initial_depths:
776
680
        for (rev_id, merge_depth, revno, end_of_merge
777
681
             ) in view_revisions:
797
701
            yield rev_id, '.'.join(map(str, revno)), merge_depth
798
702
 
799
703
 
 
704
@deprecated_function(deprecated_in((2, 2, 0)))
 
705
def calculate_view_revisions(branch, start_revision, end_revision, direction,
 
706
        specific_fileid, generate_merge_revisions):
 
707
    """Calculate the revisions to view.
 
708
 
 
709
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
 
710
             a list of the same tuples.
 
711
    """
 
712
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
 
713
        end_revision)
 
714
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
 
715
        direction, generate_merge_revisions or specific_fileid))
 
716
    if specific_fileid:
 
717
        view_revisions = _filter_revisions_touching_file_id(branch,
 
718
            specific_fileid, view_revisions,
 
719
            include_merges=generate_merge_revisions)
 
720
    return _rebase_merge_depth(view_revisions)
 
721
 
 
722
 
800
723
def _rebase_merge_depth(view_revisions):
801
724
    """Adjust depths upwards so the top level is 0."""
802
725
    # If either the first or last revision have a merge_depth of 0, we're done
846
769
    return log_rev_iterator
847
770
 
848
771
 
849
 
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
 
772
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
850
773
    """Create a filtered iterator of log_rev_iterator matching on a regex.
851
774
 
852
775
    :param branch: The branch being logged.
853
776
    :param generate_delta: Whether to generate a delta for each revision.
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.
 
777
    :param search: A user text search string.
858
778
    :param log_rev_iterator: An input iterator containing all revisions that
859
779
        could be displayed, in lists.
860
780
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
861
781
        delta).
862
782
    """
863
 
    if match is None:
 
783
    if search is None:
864
784
        return 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):
 
785
    searchRE = re_compile_checked(search, re.IGNORECASE,
 
786
            'log message filter')
 
787
    return _filter_message_re(searchRE, log_rev_iterator)
 
788
 
 
789
 
 
790
def _filter_message_re(searchRE, log_rev_iterator):
871
791
    for revs in log_rev_iterator:
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])
 
792
        new_revs = []
 
793
        for (rev_id, revno, merge_depth), rev, delta in revs:
 
794
            if searchRE.search(rev.message):
 
795
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
 
796
        yield new_revs
 
797
 
892
798
 
893
799
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
894
800
    fileids=None, direction='reverse'):
967
873
 
968
874
def _update_fileids(delta, fileids, stop_on):
969
875
    """Update the set of file-ids to search based on file lifecycle events.
970
 
 
 
876
    
971
877
    :param fileids: a set of fileids to update
972
878
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
973
879
      fileids set once their add or remove entry is detected respectively
1014
920
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1015
921
        delta).
1016
922
    """
 
923
    repository = branch.repository
1017
924
    num = 9
1018
925
    for batch in log_rev_iterator:
1019
926
        batch = iter(batch)
1068
975
    if branch_revno != 0:
1069
976
        if (start_rev_id == _mod_revision.NULL_REVISION
1070
977
            or end_rev_id == _mod_revision.NULL_REVISION):
1071
 
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
978
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
1072
979
        if start_revno > end_revno:
1073
 
            raise errors.BzrCommandError(gettext("Start revision must be "
1074
 
                                         "older than the end revision."))
 
980
            raise errors.BzrCommandError("Start revision must be older than "
 
981
                                         "the end revision.")
1075
982
    return (start_rev_id, end_rev_id)
1076
983
 
1077
984
 
1126
1033
 
1127
1034
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1128
1035
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1129
 
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1036
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
1130
1037
    if start_revno > end_revno:
1131
 
        raise errors.BzrCommandError(gettext("Start revision must be older "
1132
 
                                     "than the end revision."))
 
1038
        raise errors.BzrCommandError("Start revision must be older than "
 
1039
                                     "the end revision.")
1133
1040
 
1134
1041
    if end_revno < start_revno:
1135
1042
        return None, None, None, None
1136
1043
    cur_revno = branch_revno
1137
1044
    rev_nos = {}
1138
1045
    mainline_revs = []
1139
 
    graph = branch.repository.get_graph()
1140
 
    for revision_id in graph.iter_lefthand_ancestry(
1141
 
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1046
    for revision_id in branch.repository.iter_reverse_revision_history(
 
1047
                        branch_last_revision):
1142
1048
        if cur_revno < start_revno:
1143
1049
            # We have gone far enough, but we always add 1 more revision
1144
1050
            rev_nos[revision_id] = cur_revno
1158
1064
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1159
1065
 
1160
1066
 
 
1067
@deprecated_function(deprecated_in((2, 2, 0)))
 
1068
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
 
1069
    """Filter view_revisions based on revision ranges.
 
1070
 
 
1071
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
 
1072
            tuples to be filtered.
 
1073
 
 
1074
    :param start_rev_id: If not NONE specifies the first revision to be logged.
 
1075
            If NONE then all revisions up to the end_rev_id are logged.
 
1076
 
 
1077
    :param end_rev_id: If not NONE specifies the last revision to be logged.
 
1078
            If NONE then all revisions up to the end of the log are logged.
 
1079
 
 
1080
    :return: The filtered view_revisions.
 
1081
    """
 
1082
    if start_rev_id or end_rev_id:
 
1083
        revision_ids = [r for r, n, d in view_revisions]
 
1084
        if start_rev_id:
 
1085
            start_index = revision_ids.index(start_rev_id)
 
1086
        else:
 
1087
            start_index = 0
 
1088
        if start_rev_id == end_rev_id:
 
1089
            end_index = start_index
 
1090
        else:
 
1091
            if end_rev_id:
 
1092
                end_index = revision_ids.index(end_rev_id)
 
1093
            else:
 
1094
                end_index = len(view_revisions) - 1
 
1095
        # To include the revisions merged into the last revision,
 
1096
        # extend end_rev_id down to, but not including, the next rev
 
1097
        # with the same or lesser merge_depth
 
1098
        end_merge_depth = view_revisions[end_index][2]
 
1099
        try:
 
1100
            for index in xrange(end_index+1, len(view_revisions)+1):
 
1101
                if view_revisions[index][2] <= end_merge_depth:
 
1102
                    end_index = index - 1
 
1103
                    break
 
1104
        except IndexError:
 
1105
            # if the search falls off the end then log to the end as well
 
1106
            end_index = len(view_revisions) - 1
 
1107
        view_revisions = view_revisions[start_index:end_index+1]
 
1108
    return view_revisions
 
1109
 
 
1110
 
1161
1111
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1162
1112
    include_merges=True):
1163
1113
    r"""Return the list of revision ids which touch a given file id.
1166
1116
    This includes the revisions which directly change the file id,
1167
1117
    and the revisions which merge these changes. So if the
1168
1118
    revision graph is::
1169
 
 
1170
1119
        A-.
1171
1120
        |\ \
1172
1121
        B C E
1199
1148
    """
1200
1149
    # Lookup all possible text keys to determine which ones actually modified
1201
1150
    # the file.
1202
 
    graph = branch.repository.get_file_graph()
1203
 
    get_parent_map = graph.get_parent_map
1204
1151
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1205
1152
    next_keys = None
1206
1153
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1210
1157
    #       indexing layer. We might consider passing in hints as to the known
1211
1158
    #       access pattern (sparse/clustered, high success rate/low success
1212
1159
    #       rate). This particular access is clustered with a low success rate.
 
1160
    get_parent_map = branch.repository.texts.get_parent_map
1213
1161
    modified_text_revisions = set()
1214
1162
    chunk_size = 1000
1215
1163
    for start in xrange(0, len(text_keys), chunk_size):
1242
1190
    return result
1243
1191
 
1244
1192
 
 
1193
@deprecated_function(deprecated_in((2, 2, 0)))
 
1194
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
 
1195
                       include_merges=True):
 
1196
    """Produce an iterator of revisions to show
 
1197
    :return: an iterator of (revision_id, revno, merge_depth)
 
1198
    (if there is no revno for a revision, None is supplied)
 
1199
    """
 
1200
    if not include_merges:
 
1201
        revision_ids = mainline_revs[1:]
 
1202
        if direction == 'reverse':
 
1203
            revision_ids.reverse()
 
1204
        for revision_id in revision_ids:
 
1205
            yield revision_id, str(rev_nos[revision_id]), 0
 
1206
        return
 
1207
    graph = branch.repository.get_graph()
 
1208
    # This asks for all mainline revisions, which means we only have to spider
 
1209
    # sideways, rather than depth history. That said, its still size-of-history
 
1210
    # and should be addressed.
 
1211
    # mainline_revisions always includes an extra revision at the beginning, so
 
1212
    # don't request it.
 
1213
    parent_map = dict(((key, value) for key, value in
 
1214
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
 
1215
    # filter out ghosts; merge_sort errors on ghosts.
 
1216
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
 
1217
    merge_sorted_revisions = tsort.merge_sort(
 
1218
        rev_graph,
 
1219
        mainline_revs[-1],
 
1220
        mainline_revs,
 
1221
        generate_revno=True)
 
1222
 
 
1223
    if direction == 'forward':
 
1224
        # forward means oldest first.
 
1225
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
 
1226
    elif direction != 'reverse':
 
1227
        raise ValueError('invalid direction %r' % direction)
 
1228
 
 
1229
    for (sequence, rev_id, merge_depth, revno, end_of_merge
 
1230
         ) in merge_sorted_revisions:
 
1231
        yield rev_id, '.'.join(map(str, revno)), merge_depth
 
1232
 
 
1233
 
1245
1234
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1246
1235
    """Reverse revisions by depth.
1247
1236
 
1282
1271
    """
1283
1272
 
1284
1273
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1285
 
                 tags=None, diff=None, signature=None):
 
1274
                 tags=None, diff=None):
1286
1275
        self.rev = rev
1287
 
        if revno is None:
1288
 
            self.revno = None
1289
 
        else:
1290
 
            self.revno = str(revno)
 
1276
        self.revno = str(revno)
1291
1277
        self.merge_depth = merge_depth
1292
1278
        self.delta = delta
1293
1279
        self.tags = tags
1294
1280
        self.diff = diff
1295
 
        self.signature = signature
1296
1281
 
1297
1282
 
1298
1283
class LogFormatter(object):
1307
1292
    to indicate which LogRevision attributes it supports:
1308
1293
 
1309
1294
    - supports_delta must be True if this log formatter supports delta.
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.
 
1295
        Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1296
        attribute describes whether the 'short_status' format (1) or the long
 
1297
        one (2) should be used.
1313
1298
 
1314
1299
    - supports_merge_revisions must be True if this log formatter supports
1315
 
      merge revisions.  If not, then only mainline revisions will be passed
1316
 
      to the formatter.
 
1300
        merge revisions.  If not, then only mainline revisions will be passed
 
1301
        to the formatter.
1317
1302
 
1318
1303
    - preferred_levels is the number of levels this formatter defaults to.
1319
 
      The default value is zero meaning display all levels.
1320
 
      This value is only relevant if supports_merge_revisions is True.
 
1304
        The default value is zero meaning display all levels.
 
1305
        This value is only relevant if supports_merge_revisions is True.
1321
1306
 
1322
1307
    - supports_tags must be True if this log formatter supports tags.
1323
 
      Otherwise the tags attribute may not be populated.
 
1308
        Otherwise the tags attribute may not be populated.
1324
1309
 
1325
1310
    - supports_diff must be True if this log formatter supports diffs.
1326
 
      Otherwise the diff attribute may not be populated.
1327
 
 
1328
 
    - supports_signatures must be True if this log formatter supports GPG
1329
 
      signatures.
 
1311
        Otherwise the diff attribute may not be populated.
1330
1312
 
1331
1313
    Plugins can register functions to show custom revision properties using
1332
1314
    the properties_handler_registry. The registered function
1333
 
    must respect the following interface description::
1334
 
 
 
1315
    must respect the following interface description:
1335
1316
        def my_show_properties(properties_dict):
1336
1317
            # code that returns a dict {'name':'value'} of the properties
1337
1318
            # to be shown
1340
1321
 
1341
1322
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1342
1323
                 delta_format=None, levels=None, show_advice=False,
1343
 
                 to_exact_file=None, author_list_handler=None):
 
1324
                 to_exact_file=None):
1344
1325
        """Create a LogFormatter.
1345
1326
 
1346
1327
        :param to_file: the file to output to
1347
 
        :param to_exact_file: if set, gives an output stream to which
 
1328
        :param to_exact_file: if set, gives an output stream to which 
1348
1329
             non-Unicode diffs are written.
1349
1330
        :param show_ids: if True, revision-ids are to be displayed
1350
1331
        :param show_timezone: the timezone to use
1354
1335
          let the log formatter decide.
1355
1336
        :param show_advice: whether to show advice at the end of the
1356
1337
          log or not
1357
 
        :param author_list_handler: callable generating a list of
1358
 
          authors to display for a given revision
1359
1338
        """
1360
1339
        self.to_file = to_file
1361
1340
        # 'exact' stream used to show diff, it should print content 'as is'
1376
1355
        self.levels = levels
1377
1356
        self._show_advice = show_advice
1378
1357
        self._merge_count = 0
1379
 
        self._author_list_handler = author_list_handler
1380
1358
 
1381
1359
    def get_levels(self):
1382
1360
        """Get the number of levels to display or 0 for all."""
1401
1379
            if advice_sep:
1402
1380
                self.to_file.write(advice_sep)
1403
1381
            self.to_file.write(
1404
 
                "Use --include-merged or -n0 to see merged revisions.\n")
 
1382
                "Use --include-merges or -n0 to see merged revisions.\n")
1405
1383
 
1406
1384
    def get_advice_separator(self):
1407
1385
        """Get the text separating the log from the closing advice."""
1414
1392
        return address
1415
1393
 
1416
1394
    def short_author(self, rev):
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
 
1395
        name, address = config.parse_username(rev.get_apparent_authors()[0])
 
1396
        if name:
 
1397
            return name
 
1398
        return address
1452
1399
 
1453
1400
    def merge_marker(self, revision):
1454
1401
        """Get the merge marker to include in the output or '' if none."""
1524
1471
    supports_delta = True
1525
1472
    supports_tags = True
1526
1473
    supports_diff = True
1527
 
    supports_signatures = True
1528
1474
 
1529
1475
    def __init__(self, *args, **kwargs):
1530
1476
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1550
1496
                self.merge_marker(revision)))
1551
1497
        if revision.tags:
1552
1498
            lines.append('tags: %s' % (', '.join(revision.tags)))
1553
 
        if self.show_ids or revision.revno is None:
 
1499
        if self.show_ids:
1554
1500
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1555
 
        if self.show_ids:
1556
1501
            for parent_id in revision.rev.parent_ids:
1557
1502
                lines.append('parent: %s' % (parent_id,))
1558
1503
        lines.extend(self.custom_properties(revision.rev))
1559
1504
 
1560
1505
        committer = revision.rev.committer
1561
 
        authors = self.authors(revision.rev, 'all')
 
1506
        authors = revision.rev.get_apparent_authors()
1562
1507
        if authors != [committer]:
1563
1508
            lines.append('author: %s' % (", ".join(authors),))
1564
1509
        lines.append('committer: %s' % (committer,))
1569
1514
 
1570
1515
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1571
1516
 
1572
 
        if revision.signature is not None:
1573
 
            lines.append('signature: ' + revision.signature)
1574
 
 
1575
1517
        lines.append('message:')
1576
1518
        if not revision.rev.message:
1577
1519
            lines.append('  (no message)')
1584
1526
        to_file = self.to_file
1585
1527
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1586
1528
        if revision.delta is not None:
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)
 
1529
            # We don't respect delta_format for compatibility
 
1530
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1531
                                short_status=False)
1591
1532
        if revision.diff is not None:
1592
1533
            to_file.write(indent + 'diff:\n')
1593
1534
            to_file.flush()
1624
1565
        indent = '    ' * depth
1625
1566
        revno_width = self.revno_width_by_depth.get(depth)
1626
1567
        if revno_width is None:
1627
 
            if revision.revno is None or revision.revno.find('.') == -1:
 
1568
            if revision.revno.find('.') == -1:
1628
1569
                # mainline revno, e.g. 12345
1629
1570
                revno_width = 5
1630
1571
            else:
1638
1579
        if revision.tags:
1639
1580
            tags = ' {%s}' % (', '.join(revision.tags))
1640
1581
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1641
 
                revision.revno or "", self.short_author(revision.rev),
 
1582
                revision.revno, self.short_author(revision.rev),
1642
1583
                format_date(revision.rev.timestamp,
1643
1584
                            revision.rev.timezone or 0,
1644
1585
                            self.show_timezone, date_fmt="%Y-%m-%d",
1645
1586
                            show_offset=False),
1646
1587
                tags, self.merge_marker(revision)))
1647
1588
        self.show_properties(revision.rev, indent+offset)
1648
 
        if self.show_ids or revision.revno is None:
 
1589
        if self.show_ids:
1649
1590
            to_file.write(indent + offset + 'revision-id:%s\n'
1650
1591
                          % (revision.rev.revision_id,))
1651
1592
        if not revision.rev.message:
1656
1597
                to_file.write(indent + offset + '%s\n' % (l,))
1657
1598
 
1658
1599
        if revision.delta is not None:
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)
 
1600
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1601
                                short_status=self.delta_format==1)
1664
1602
        if revision.diff is not None:
1665
1603
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1666
1604
        to_file.write('\n')
1704
1642
 
1705
1643
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1706
1644
        """Format log info into one string. Truncate tail of 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
 
1645
        :param  revno:      revision number or None.
 
1646
                            Revision numbers counts from 1.
 
1647
        :param  rev:        revision object
 
1648
        :param  max_chars:  maximum length of resulting string
 
1649
        :param  tags:       list of tags or None
 
1650
        :param  prefix:     string to prefix each line
 
1651
        :return:            formatted truncated string
1715
1652
        """
1716
1653
        out = []
1717
1654
        if revno:
1718
1655
            # show revno only when is not None
1719
1656
            out.append("%s:" % revno)
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))
 
1657
        out.append(self.truncate(self.short_author(rev), 20))
1724
1658
        out.append(self.date_string(rev))
1725
1659
        if len(rev.parent_ids) > 1:
1726
1660
            out.append('[merge]')
1745
1679
                               self.show_timezone,
1746
1680
                               date_fmt='%Y-%m-%d',
1747
1681
                               show_offset=False)
1748
 
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1749
 
        committer_str = committer_str.replace(' <', '  <')
 
1682
        committer_str = revision.rev.committer.replace (' <', '  <')
1750
1683
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1751
1684
 
1752
1685
        if revision.delta is not None and revision.delta.has_changed():
1785
1718
        return self.get(name)(*args, **kwargs)
1786
1719
 
1787
1720
    def get_default(self, branch):
1788
 
        c = branch.get_config_stack()
1789
 
        return self.get(c.get('log_format'))
 
1721
        return self.get(branch.get_config().log_format())
1790
1722
 
1791
1723
 
1792
1724
log_formatter_registry = LogFormatterRegistry()
1793
1725
 
1794
1726
 
1795
1727
log_formatter_registry.register('short', ShortLogFormatter,
1796
 
                                'Moderately short log format.')
 
1728
                                'Moderately short log format')
1797
1729
log_formatter_registry.register('long', LongLogFormatter,
1798
 
                                'Detailed log format.')
 
1730
                                'Detailed log format')
1799
1731
log_formatter_registry.register('line', LineLogFormatter,
1800
 
                                'Log format with one line per revision.')
 
1732
                                'Log format with one line per revision')
1801
1733
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1802
 
                                'Format used by GNU ChangeLog files.')
 
1734
                                'Format used by GNU ChangeLog files')
1803
1735
 
1804
1736
 
1805
1737
def register_formatter(name, formatter):
1815
1747
    try:
1816
1748
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1817
1749
    except KeyError:
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')
 
1750
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1751
 
 
1752
 
 
1753
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
 
1754
    # deprecated; for compatibility
 
1755
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
 
1756
    lf.show(revno, rev, delta)
1847
1757
 
1848
1758
 
1849
1759
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1914
1824
    old_revisions = set()
1915
1825
    new_history = []
1916
1826
    new_revisions = set()
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)
 
1827
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
 
1828
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
1920
1829
    stop_revision = None
1921
1830
    do_old = True
1922
1831
    do_new = True
1997
1906
        lf.log_revision(lr)
1998
1907
 
1999
1908
 
2000
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
1909
def _get_info_for_log_files(revisionspec_list, file_list):
2001
1910
    """Find file-ids and kinds given a list of files and a revision range.
2002
1911
 
2003
1912
    We search for files at the end of the range. If not found there,
2007
1916
    :param file_list: the list of paths given on the command line;
2008
1917
      the first of these can be a branch location or a file path,
2009
1918
      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.
2012
1919
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2013
1920
      info_list is a list of (relative_path, file_id, kind) tuples where
2014
1921
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2015
1922
      branch will be read-locked.
2016
1923
    """
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)
 
1924
    from builtins import _get_revision_range, safe_relpath_files
 
1925
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
1926
    b.lock_read()
2021
1927
    # XXX: It's damn messy converting a list of paths to relative paths when
2022
1928
    # those paths might be deleted ones, they might be on a case-insensitive
2023
1929
    # filesystem and/or they might be in silly locations (like another branch).
2027
1933
    # case of running log in a nested directory, assuming paths beyond the
2028
1934
    # first one haven't been deleted ...
2029
1935
    if tree:
2030
 
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
1936
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2031
1937
    else:
2032
1938
        relpaths = [path] + file_list[1:]
2033
1939
    info_list = []
2111
2017
                          len(row) > 1 and row[1] == 'fixed']
2112
2018
 
2113
2019
        if fixed_bug_urls:
2114
 
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
2115
 
                    ' '.join(fixed_bug_urls)}
 
2020
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2116
2021
    return {}
2117
2022
 
2118
2023
properties_handler_registry.register('bugs_properties_handler',