~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Vincent Ladeuil
  • Date: 2010-03-10 09:33:04 UTC
  • mto: (5082.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5083.
  • Revision ID: v.ladeuil+lp@free.fr-20100310093304-4245t4tazd4sxoav
Cleanup test from overly cautious checks.

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
630
553
                    # may not raise _StartNotLinearAncestor for a revision that
631
554
                    # is an ancestor but not a *linear* one. But since we have
632
555
                    # loaded the graph to do the check (or calculate a dotted
633
 
                    # revno), we may as well accept to show the log...  We need
634
 
                    # the check only if start_rev_id is not None as all
635
 
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
636
 
                    # -- vila 20100319
 
556
                    # revno), we may as well accept to show the log... 
 
557
                    # -- vila 100201
637
558
                    graph = branch.repository.get_graph()
638
 
                    if (start_rev_id is not None
639
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
559
                    if not graph.is_ancestor(start_rev_id, end_rev_id):
640
560
                        raise _StartNotLinearAncestor()
641
 
                    # Since we collected the revisions so far, we need to
642
 
                    # adjust end_rev_id.
643
561
                    end_rev_id = rev_id
644
562
                    break
645
563
                else:
646
564
                    initial_revisions.append((rev_id, revno, depth))
647
565
            else:
648
566
                # No merged revisions found
649
 
                return initial_revisions
 
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)
650
573
        except _StartNotLinearAncestor:
651
574
            # A merge was never detected so the lower revision limit can't
652
575
            # be nested down somewhere
653
 
            raise errors.BzrCommandError(gettext('Start revision not found in'
654
 
                ' history of end revision.'))
655
 
 
656
 
    # We exit the loop above because we encounter a revision with merges, from
657
 
    # this revision, we need to switch to _graph_view_revisions.
 
576
            raise errors.BzrCommandError('Start revision not found in'
 
577
                ' history of end revision.')
658
578
 
659
579
    # A log including nested merges is required. If the direction is reverse,
660
580
    # we rebase the initial merge depths so that the development line is
663
583
    # indented at the end seems slightly nicer in that case.
664
584
    view_revisions = chain(iter(initial_revisions),
665
585
        _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
 
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)
669
595
 
670
596
 
671
597
def _has_merges(branch, rev_id):
674
600
    return len(parents) > 1
675
601
 
676
602
 
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
603
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
693
604
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
694
605
    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
 
606
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
607
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
701
608
        if len(start_dotted) == 1 and len(end_dotted) == 1:
702
609
            # both on mainline
703
610
            return start_dotted[0] <= end_dotted[0]
713
620
    return True
714
621
 
715
622
 
716
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
717
 
                           exclude_common_ancestry=False):
 
623
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
718
624
    """Calculate a sequence of revisions to view, newest to oldest.
719
625
 
720
626
    :param start_rev_id: the lower revision-id
721
627
    :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
628
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
725
629
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
726
 
        is not found walking the left-hand history
 
630
      is not found walking the left-hand history
727
631
    """
728
632
    br_revno, br_rev_id = branch.last_revision_info()
729
633
    repo = branch.repository
730
 
    graph = repo.get_graph()
731
634
    if start_rev_id is None and end_rev_id is None:
732
635
        cur_revno = br_revno
733
 
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
734
 
            (_mod_revision.NULL_REVISION,)):
 
636
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
735
637
            yield revision_id, str(cur_revno), 0
736
638
            cur_revno -= 1
737
639
    else:
738
640
        if end_rev_id is None:
739
641
            end_rev_id = br_rev_id
740
642
        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)
 
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)
744
646
            if not found_start and revision_id == start_rev_id:
745
 
                if not exclude_common_ancestry:
746
 
                    yield revision_id, revno_str, 0
 
647
                yield revision_id, revno_str, 0
747
648
                found_start = True
748
649
                break
749
650
            else:
754
655
 
755
656
 
756
657
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
757
 
                          rebase_initial_depths=True,
758
 
                          exclude_common_ancestry=False):
 
658
    rebase_initial_depths=True):
759
659
    """Calculate revisions to view including merges, newest to oldest.
760
660
 
761
661
    :param branch: the branch
765
665
      revision is found?
766
666
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
767
667
    """
768
 
    if exclude_common_ancestry:
769
 
        stop_rule = 'with-merges-without-common-ancestry'
770
 
    else:
771
 
        stop_rule = 'with-merges'
772
668
    view_revisions = branch.iter_merge_sorted_revisions(
773
669
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
774
 
        stop_rule=stop_rule)
 
670
        stop_rule="with-merges")
775
671
    if not rebase_initial_depths:
776
672
        for (rev_id, merge_depth, revno, end_of_merge
777
673
             ) in view_revisions:
797
693
            yield rev_id, '.'.join(map(str, revno)), merge_depth
798
694
 
799
695
 
 
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
 
800
715
def _rebase_merge_depth(view_revisions):
801
716
    """Adjust depths upwards so the top level is 0."""
802
717
    # If either the first or last revision have a merge_depth of 0, we're done
846
761
    return log_rev_iterator
847
762
 
848
763
 
849
 
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
 
764
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
850
765
    """Create a filtered iterator of log_rev_iterator matching on a regex.
851
766
 
852
767
    :param branch: The branch being logged.
853
768
    :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.
 
769
    :param search: A user text search string.
858
770
    :param log_rev_iterator: An input iterator containing all revisions that
859
771
        could be displayed, in lists.
860
772
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
861
773
        delta).
862
774
    """
863
 
    if match is None:
 
775
    if search is None:
864
776
        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):
 
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):
871
783
    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])
 
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
 
892
790
 
893
791
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
894
792
    fileids=None, direction='reverse'):
967
865
 
968
866
def _update_fileids(delta, fileids, stop_on):
969
867
    """Update the set of file-ids to search based on file lifecycle events.
970
 
 
 
868
    
971
869
    :param fileids: a set of fileids to update
972
870
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
973
871
      fileids set once their add or remove entry is detected respectively
1014
912
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1015
913
        delta).
1016
914
    """
 
915
    repository = branch.repository
1017
916
    num = 9
1018
917
    for batch in log_rev_iterator:
1019
918
        batch = iter(batch)
1068
967
    if branch_revno != 0:
1069
968
        if (start_rev_id == _mod_revision.NULL_REVISION
1070
969
            or end_rev_id == _mod_revision.NULL_REVISION):
1071
 
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
970
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
1072
971
        if start_revno > end_revno:
1073
 
            raise errors.BzrCommandError(gettext("Start revision must be "
1074
 
                                         "older than the end revision."))
 
972
            raise errors.BzrCommandError("Start revision must be older than "
 
973
                                         "the end revision.")
1075
974
    return (start_rev_id, end_rev_id)
1076
975
 
1077
976
 
1126
1025
 
1127
1026
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1128
1027
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1129
 
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1028
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
1130
1029
    if start_revno > end_revno:
1131
 
        raise errors.BzrCommandError(gettext("Start revision must be older "
1132
 
                                     "than the end revision."))
 
1030
        raise errors.BzrCommandError("Start revision must be older than "
 
1031
                                     "the end revision.")
1133
1032
 
1134
1033
    if end_revno < start_revno:
1135
1034
        return None, None, None, None
1136
1035
    cur_revno = branch_revno
1137
1036
    rev_nos = {}
1138
1037
    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,)):
 
1038
    for revision_id in branch.repository.iter_reverse_revision_history(
 
1039
                        branch_last_revision):
1142
1040
        if cur_revno < start_revno:
1143
1041
            # We have gone far enough, but we always add 1 more revision
1144
1042
            rev_nos[revision_id] = cur_revno
1158
1056
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1159
1057
 
1160
1058
 
 
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
 
1161
1103
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1162
1104
    include_merges=True):
1163
1105
    r"""Return the list of revision ids which touch a given file id.
1166
1108
    This includes the revisions which directly change the file id,
1167
1109
    and the revisions which merge these changes. So if the
1168
1110
    revision graph is::
1169
 
 
1170
1111
        A-.
1171
1112
        |\ \
1172
1113
        B C E
1199
1140
    """
1200
1141
    # Lookup all possible text keys to determine which ones actually modified
1201
1142
    # the file.
1202
 
    graph = branch.repository.get_file_graph()
1203
 
    get_parent_map = graph.get_parent_map
1204
1143
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1205
1144
    next_keys = None
1206
1145
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1210
1149
    #       indexing layer. We might consider passing in hints as to the known
1211
1150
    #       access pattern (sparse/clustered, high success rate/low success
1212
1151
    #       rate). This particular access is clustered with a low success rate.
 
1152
    get_parent_map = branch.repository.texts.get_parent_map
1213
1153
    modified_text_revisions = set()
1214
1154
    chunk_size = 1000
1215
1155
    for start in xrange(0, len(text_keys), chunk_size):
1242
1182
    return result
1243
1183
 
1244
1184
 
 
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
 
1245
1226
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1246
1227
    """Reverse revisions by depth.
1247
1228
 
1282
1263
    """
1283
1264
 
1284
1265
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1285
 
                 tags=None, diff=None, signature=None):
 
1266
                 tags=None, diff=None):
1286
1267
        self.rev = rev
1287
 
        if revno is None:
1288
 
            self.revno = None
1289
 
        else:
1290
 
            self.revno = str(revno)
 
1268
        self.revno = str(revno)
1291
1269
        self.merge_depth = merge_depth
1292
1270
        self.delta = delta
1293
1271
        self.tags = tags
1294
1272
        self.diff = diff
1295
 
        self.signature = signature
1296
1273
 
1297
1274
 
1298
1275
class LogFormatter(object):
1307
1284
    to indicate which LogRevision attributes it supports:
1308
1285
 
1309
1286
    - 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.
 
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.
1313
1290
 
1314
1291
    - 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.
 
1292
        merge revisions.  If not, then only mainline revisions will be passed
 
1293
        to the formatter.
1317
1294
 
1318
1295
    - 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.
 
1296
        The default value is zero meaning display all levels.
 
1297
        This value is only relevant if supports_merge_revisions is True.
1321
1298
 
1322
1299
    - supports_tags must be True if this log formatter supports tags.
1323
 
      Otherwise the tags attribute may not be populated.
 
1300
        Otherwise the tags attribute may not be populated.
1324
1301
 
1325
1302
    - 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.
 
1303
        Otherwise the diff attribute may not be populated.
1330
1304
 
1331
1305
    Plugins can register functions to show custom revision properties using
1332
1306
    the properties_handler_registry. The registered function
1333
 
    must respect the following interface description::
1334
 
 
 
1307
    must respect the following interface description:
1335
1308
        def my_show_properties(properties_dict):
1336
1309
            # code that returns a dict {'name':'value'} of the properties
1337
1310
            # to be shown
1340
1313
 
1341
1314
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1342
1315
                 delta_format=None, levels=None, show_advice=False,
1343
 
                 to_exact_file=None, author_list_handler=None):
 
1316
                 to_exact_file=None):
1344
1317
        """Create a LogFormatter.
1345
1318
 
1346
1319
        :param to_file: the file to output to
1347
 
        :param to_exact_file: if set, gives an output stream to which
 
1320
        :param to_exact_file: if set, gives an output stream to which 
1348
1321
             non-Unicode diffs are written.
1349
1322
        :param show_ids: if True, revision-ids are to be displayed
1350
1323
        :param show_timezone: the timezone to use
1354
1327
          let the log formatter decide.
1355
1328
        :param show_advice: whether to show advice at the end of the
1356
1329
          log or not
1357
 
        :param author_list_handler: callable generating a list of
1358
 
          authors to display for a given revision
1359
1330
        """
1360
1331
        self.to_file = to_file
1361
1332
        # 'exact' stream used to show diff, it should print content 'as is'
1376
1347
        self.levels = levels
1377
1348
        self._show_advice = show_advice
1378
1349
        self._merge_count = 0
1379
 
        self._author_list_handler = author_list_handler
1380
1350
 
1381
1351
    def get_levels(self):
1382
1352
        """Get the number of levels to display or 0 for all."""
1401
1371
            if advice_sep:
1402
1372
                self.to_file.write(advice_sep)
1403
1373
            self.to_file.write(
1404
 
                "Use --include-merged or -n0 to see merged revisions.\n")
 
1374
                "Use --include-merges or -n0 to see merged revisions.\n")
1405
1375
 
1406
1376
    def get_advice_separator(self):
1407
1377
        """Get the text separating the log from the closing advice."""
1414
1384
        return address
1415
1385
 
1416
1386
    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
 
1387
        name, address = config.parse_username(rev.get_apparent_authors()[0])
 
1388
        if name:
 
1389
            return name
 
1390
        return address
1452
1391
 
1453
1392
    def merge_marker(self, revision):
1454
1393
        """Get the merge marker to include in the output or '' if none."""
1485
1424
        """
1486
1425
        # Revision comes directly from a foreign repository
1487
1426
        if isinstance(rev, foreign.ForeignRevision):
1488
 
            return self._format_properties(
1489
 
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1427
            return self._format_properties(rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1490
1428
 
1491
1429
        # Imported foreign revision revision ids always contain :
1492
1430
        if not ":" in rev.revision_id:
1524
1462
    supports_delta = True
1525
1463
    supports_tags = True
1526
1464
    supports_diff = True
1527
 
    supports_signatures = True
1528
1465
 
1529
1466
    def __init__(self, *args, **kwargs):
1530
1467
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1550
1487
                self.merge_marker(revision)))
1551
1488
        if revision.tags:
1552
1489
            lines.append('tags: %s' % (', '.join(revision.tags)))
1553
 
        if self.show_ids or revision.revno is None:
 
1490
        if self.show_ids:
1554
1491
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1555
 
        if self.show_ids:
1556
1492
            for parent_id in revision.rev.parent_ids:
1557
1493
                lines.append('parent: %s' % (parent_id,))
1558
1494
        lines.extend(self.custom_properties(revision.rev))
1559
1495
 
1560
1496
        committer = revision.rev.committer
1561
 
        authors = self.authors(revision.rev, 'all')
 
1497
        authors = revision.rev.get_apparent_authors()
1562
1498
        if authors != [committer]:
1563
1499
            lines.append('author: %s' % (", ".join(authors),))
1564
1500
        lines.append('committer: %s' % (committer,))
1569
1505
 
1570
1506
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1571
1507
 
1572
 
        if revision.signature is not None:
1573
 
            lines.append('signature: ' + revision.signature)
1574
 
 
1575
1508
        lines.append('message:')
1576
1509
        if not revision.rev.message:
1577
1510
            lines.append('  (no message)')
1584
1517
        to_file = self.to_file
1585
1518
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1586
1519
        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)
 
1520
            # We don't respect delta_format for compatibility
 
1521
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1522
                                short_status=False)
1591
1523
        if revision.diff is not None:
1592
1524
            to_file.write(indent + 'diff:\n')
1593
1525
            to_file.flush()
1624
1556
        indent = '    ' * depth
1625
1557
        revno_width = self.revno_width_by_depth.get(depth)
1626
1558
        if revno_width is None:
1627
 
            if revision.revno is None or revision.revno.find('.') == -1:
 
1559
            if revision.revno.find('.') == -1:
1628
1560
                # mainline revno, e.g. 12345
1629
1561
                revno_width = 5
1630
1562
            else:
1638
1570
        if revision.tags:
1639
1571
            tags = ' {%s}' % (', '.join(revision.tags))
1640
1572
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1641
 
                revision.revno or "", self.short_author(revision.rev),
 
1573
                revision.revno, self.short_author(revision.rev),
1642
1574
                format_date(revision.rev.timestamp,
1643
1575
                            revision.rev.timezone or 0,
1644
1576
                            self.show_timezone, date_fmt="%Y-%m-%d",
1645
1577
                            show_offset=False),
1646
1578
                tags, self.merge_marker(revision)))
1647
1579
        self.show_properties(revision.rev, indent+offset)
1648
 
        if self.show_ids or revision.revno is None:
 
1580
        if self.show_ids:
1649
1581
            to_file.write(indent + offset + 'revision-id:%s\n'
1650
1582
                          % (revision.rev.revision_id,))
1651
1583
        if not revision.rev.message:
1656
1588
                to_file.write(indent + offset + '%s\n' % (l,))
1657
1589
 
1658
1590
        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)
 
1591
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1592
                                short_status=self.delta_format==1)
1664
1593
        if revision.diff is not None:
1665
1594
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1666
1595
        to_file.write('\n')
1704
1633
 
1705
1634
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1706
1635
        """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
 
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
1715
1643
        """
1716
1644
        out = []
1717
1645
        if revno:
1718
1646
            # show revno only when is not None
1719
1647
            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))
 
1648
        out.append(self.truncate(self.short_author(rev), 20))
1724
1649
        out.append(self.date_string(rev))
1725
1650
        if len(rev.parent_ids) > 1:
1726
1651
            out.append('[merge]')
1745
1670
                               self.show_timezone,
1746
1671
                               date_fmt='%Y-%m-%d',
1747
1672
                               show_offset=False)
1748
 
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1749
 
        committer_str = committer_str.replace(' <', '  <')
 
1673
        committer_str = revision.rev.committer.replace (' <', '  <')
1750
1674
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1751
1675
 
1752
1676
        if revision.delta is not None and revision.delta.has_changed():
1785
1709
        return self.get(name)(*args, **kwargs)
1786
1710
 
1787
1711
    def get_default(self, branch):
1788
 
        c = branch.get_config_stack()
1789
 
        return self.get(c.get('log_format'))
 
1712
        return self.get(branch.get_config().log_format())
1790
1713
 
1791
1714
 
1792
1715
log_formatter_registry = LogFormatterRegistry()
1793
1716
 
1794
1717
 
1795
1718
log_formatter_registry.register('short', ShortLogFormatter,
1796
 
                                'Moderately short log format.')
 
1719
                                'Moderately short log format')
1797
1720
log_formatter_registry.register('long', LongLogFormatter,
1798
 
                                'Detailed log format.')
 
1721
                                'Detailed log format')
1799
1722
log_formatter_registry.register('line', LineLogFormatter,
1800
 
                                'Log format with one line per revision.')
 
1723
                                'Log format with one line per revision')
1801
1724
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1802
 
                                'Format used by GNU ChangeLog files.')
 
1725
                                'Format used by GNU ChangeLog files')
1803
1726
 
1804
1727
 
1805
1728
def register_formatter(name, formatter):
1815
1738
    try:
1816
1739
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1817
1740
    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')
 
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)
1847
1748
 
1848
1749
 
1849
1750
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1914
1815
    old_revisions = set()
1915
1816
    new_history = []
1916
1817
    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)
 
1818
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
 
1819
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
1920
1820
    stop_revision = None
1921
1821
    do_old = True
1922
1822
    do_new = True
1997
1897
        lf.log_revision(lr)
1998
1898
 
1999
1899
 
2000
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
1900
def _get_info_for_log_files(revisionspec_list, file_list):
2001
1901
    """Find file-ids and kinds given a list of files and a revision range.
2002
1902
 
2003
1903
    We search for files at the end of the range. If not found there,
2007
1907
    :param file_list: the list of paths given on the command line;
2008
1908
      the first of these can be a branch location or a file path,
2009
1909
      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
1910
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2013
1911
      info_list is a list of (relative_path, file_id, kind) tuples where
2014
1912
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2015
1913
      branch will be read-locked.
2016
1914
    """
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)
 
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()
2021
1918
    # XXX: It's damn messy converting a list of paths to relative paths when
2022
1919
    # those paths might be deleted ones, they might be on a case-insensitive
2023
1920
    # filesystem and/or they might be in silly locations (like another branch).
2027
1924
    # case of running log in a nested directory, assuming paths beyond the
2028
1925
    # first one haven't been deleted ...
2029
1926
    if tree:
2030
 
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
1927
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2031
1928
    else:
2032
1929
        relpaths = [path] + file_list[1:]
2033
1930
    info_list = []
2109
2006
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2110
2007
        fixed_bug_urls = [row[0] for row in bug_rows if
2111
2008
                          len(row) > 1 and row[1] == 'fixed']
2112
 
 
 
2009
        
2113
2010
        if fixed_bug_urls:
2114
 
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
2115
 
                    ' '.join(fixed_bug_urls)}
 
2011
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
2116
2012
    return {}
2117
2013
 
2118
2014
properties_handler_registry.register('bugs_properties_handler',