/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/log.py

  • Committer: Jelmer Vernooij
  • Date: 2017-12-04 23:01:39 UTC
  • mto: This revision was merged to the branch mainline in revision 6839.
  • Revision ID: jelmer@jelmer.uk-20171204230139-1sc3c18ikwewdejm
Remove bytes_to_gzip; work with chunks instead.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
 
18
 
 
19
17
"""Code to show logs of changes.
20
18
 
21
19
Various flavors of log can be produced:
49
47
all the changes since the previous revision that touched hello.c.
50
48
"""
51
49
 
 
50
from __future__ import absolute_import
 
51
 
52
52
import codecs
53
 
from cStringIO import StringIO
54
 
from itertools import (
55
 
    chain,
56
 
    izip,
57
 
    )
 
53
import itertools
58
54
import re
59
55
import sys
60
56
from warnings import (
61
57
    warn,
62
58
    )
63
59
 
64
 
from bzrlib.lazy_import import lazy_import
 
60
from .lazy_import import lazy_import
65
61
lazy_import(globals(), """
66
62
 
67
 
from bzrlib import (
68
 
    bzrdir,
 
63
from breezy import (
69
64
    config,
 
65
    controldir,
70
66
    diff,
71
67
    errors,
72
68
    foreign,
73
69
    repository as _mod_repository,
74
70
    revision as _mod_revision,
75
 
    revisionspec,
76
 
    trace,
77
71
    tsort,
78
72
    )
 
73
from breezy.i18n import gettext, ngettext
79
74
""")
80
75
 
81
 
from bzrlib import (
 
76
from . import (
 
77
    lazy_regex,
82
78
    registry,
 
79
    revisionspec,
83
80
    )
84
 
from bzrlib.osutils import (
 
81
from .osutils import (
85
82
    format_date,
86
83
    format_date_with_offset_in_original_timezone,
 
84
    get_diff_header_encoding,
87
85
    get_terminal_encoding,
88
 
    re_compile_checked,
89
86
    terminal_width,
90
87
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
 
88
from breezy.sixish import (
 
89
    BytesIO,
 
90
    range,
 
91
    zip,
94
92
    )
95
93
 
96
94
 
105
103
    TODO: Perhaps some way to limit this to only particular revisions,
106
104
    or to traverse a non-mainline set of revisions?
107
105
    """
108
 
    last_ie = None
 
106
    last_verifier = None
109
107
    last_path = None
110
108
    revno = 1
111
 
    for revision_id in branch.revision_history():
112
 
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if file_id in this_inv:
114
 
            this_ie = this_inv[file_id]
115
 
            this_path = this_inv.id2path(file_id)
 
109
    graph = branch.repository.get_graph()
 
110
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
111
        [_mod_revision.NULL_REVISION]))
 
112
    for revision_id in reversed(history):
 
113
        this_tree = branch.repository.revision_tree(revision_id)
 
114
        try:
 
115
            this_path = this_tree.id2path(file_id)
 
116
        except errors.NoSuchId:
 
117
            this_verifier = this_path = None
116
118
        else:
117
 
            this_ie = this_path = None
 
119
            this_verifier = this_tree.get_file_verifier(this_path, file_id)
118
120
 
119
121
        # now we know how it was last time, and how it is in this revision.
120
122
        # are those two states effectively the same or not?
121
123
 
122
 
        if not this_ie and not last_ie:
 
124
        if not this_verifier and not last_verifier:
123
125
            # not present in either
124
126
            pass
125
 
        elif this_ie and not last_ie:
 
127
        elif this_verifier and not last_verifier:
126
128
            yield revno, revision_id, "added " + this_path
127
 
        elif not this_ie and last_ie:
 
129
        elif not this_verifier and last_verifier:
128
130
            # deleted here
129
131
            yield revno, revision_id, "deleted " + last_path
130
132
        elif this_path != last_path:
131
133
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
132
 
        elif (this_ie.text_size != last_ie.text_size
133
 
              or this_ie.text_sha1 != last_ie.text_sha1):
 
134
        elif (this_verifier != last_verifier):
134
135
            yield revno, revision_id, "modified " + this_path
135
136
 
136
 
        last_ie = this_ie
 
137
        last_verifier = this_verifier
137
138
        last_path = this_path
138
139
        revno += 1
139
140
 
140
141
 
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
 
 
150
142
def show_log(branch,
151
143
             lf,
152
144
             specific_fileid=None,
156
148
             end_revision=None,
157
149
             search=None,
158
150
             limit=None,
159
 
             show_diff=False):
 
151
             show_diff=False,
 
152
             match=None):
160
153
    """Write out human-readable log of commits to this branch.
161
154
 
162
155
    This function is being retained for backwards compatibility but
185
178
        if None or 0.
186
179
 
187
180
    :param show_diff: If True, output a diff after each revision.
 
181
 
 
182
    :param match: Dictionary of search lists to use when matching revision
 
183
      properties.
188
184
    """
189
185
    # Convert old-style parameters to new-style parameters
190
186
    if specific_fileid is not None:
214
210
    Logger(branch, rqst).show(lf)
215
211
 
216
212
 
217
 
# Note: This needs to be kept this in sync with the defaults in
 
213
# Note: This needs to be kept in sync with the defaults in
218
214
# make_log_request_dict() below
219
215
_DEFAULT_REQUEST_PARAMS = {
220
216
    'direction': 'reverse',
221
 
    'levels': 1,
 
217
    'levels': None,
222
218
    'generate_tags': True,
223
219
    'exclude_common_ancestry': False,
224
220
    '_match_using_deltas': True,
227
223
 
228
224
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
225
                          start_revision=None, end_revision=None, limit=None,
230
 
                          message_search=None, levels=1, generate_tags=True,
 
226
                          message_search=None, levels=None, generate_tags=True,
231
227
                          delta_type=None,
232
228
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
 
229
                          exclude_common_ancestry=False, match=None,
 
230
                          signature=False, omit_merges=False,
234
231
                          ):
235
232
    """Convenience function for making a logging request dictionary.
236
233
 
257
254
      matching commit messages
258
255
 
259
256
    :param levels: the number of levels of revisions to
260
 
      generate; 1 for just the mainline; 0 for all levels.
 
257
      generate; 1 for just the mainline; 0 for all levels, or None for
 
258
      a sensible default.
261
259
 
262
260
    :param generate_tags: If True, include tags for matched revisions.
263
 
 
 
261
`
264
262
    :param delta_type: Either 'full', 'partial' or None.
265
263
      'full' means generate the complete delta - adds/deletes/modifies/etc;
266
264
      'partial' means filter the delta using specific_fileids;
273
271
 
274
272
    :param _match_using_deltas: a private parameter controlling the
275
273
      algorithm used for matching specific_fileids. This parameter
276
 
      may be removed in the future so bzrlib client code should NOT
 
274
      may be removed in the future so breezy client code should NOT
277
275
      use it.
278
276
 
279
277
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
278
      range operator or as a graph difference.
 
279
 
 
280
    :param signature: show digital signature information
 
281
 
 
282
    :param match: Dictionary of list of search strings to use when filtering
 
283
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
284
      the empty string to match any of the preceding properties.
 
285
 
 
286
    :param omit_merges: If True, commits with more than one parent are
 
287
      omitted.
 
288
 
281
289
    """
 
290
    # Take care of old style message_search parameter
 
291
    if message_search:
 
292
        if match:
 
293
            if 'message' in match:
 
294
                match['message'].append(message_search)
 
295
            else:
 
296
                match['message'] = [message_search]
 
297
        else:
 
298
            match={ 'message': [message_search] }
282
299
    return {
283
300
        'direction': direction,
284
301
        'specific_fileids': specific_fileids,
285
302
        'start_revision': start_revision,
286
303
        'end_revision': end_revision,
287
304
        'limit': limit,
288
 
        'message_search': message_search,
289
305
        'levels': levels,
290
306
        'generate_tags': generate_tags,
291
307
        'delta_type': delta_type,
292
308
        'diff_type': diff_type,
293
309
        'exclude_common_ancestry': exclude_common_ancestry,
 
310
        'signature': signature,
 
311
        'match': match,
 
312
        'omit_merges': omit_merges,
294
313
        # Add 'private' attributes for features that may be deprecated
295
314
        '_match_using_deltas': _match_using_deltas,
296
315
    }
298
317
 
299
318
def _apply_log_request_defaults(rqst):
300
319
    """Apply default values to a request dictionary."""
301
 
    result = _DEFAULT_REQUEST_PARAMS
 
320
    result = _DEFAULT_REQUEST_PARAMS.copy()
302
321
    if rqst:
303
322
        result.update(rqst)
304
323
    return result
305
324
 
306
325
 
 
326
def format_signature_validity(rev_id, branch):
 
327
    """get the signature validity
 
328
 
 
329
    :param rev_id: revision id to validate
 
330
    :param branch: branch of revision
 
331
    :return: human readable string to print to log
 
332
    """
 
333
    from breezy import gpg
 
334
 
 
335
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
 
336
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
 
337
    if result[0] == gpg.SIGNATURE_VALID:
 
338
        return u"valid signature from {0}".format(result[1])
 
339
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
340
        return "unknown key {0}".format(result[1])
 
341
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
342
        return "invalid signature!"
 
343
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
344
        return "no signature"
 
345
 
 
346
 
307
347
class LogGenerator(object):
308
348
    """A generator of log revisions."""
309
349
 
354
394
        # Tweak the LogRequest based on what the LogFormatter can handle.
355
395
        # (There's no point generating stuff if the formatter can't display it.)
356
396
        rqst = self.rqst
357
 
        rqst['levels'] = lf.get_levels()
 
397
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
398
            # user didn't specify levels, use whatever the LF can handle:
 
399
            rqst['levels'] = lf.get_levels()
 
400
 
358
401
        if not getattr(lf, 'supports_tags', False):
359
402
            rqst['generate_tags'] = False
360
403
        if not getattr(lf, 'supports_delta', False):
361
404
            rqst['delta_type'] = None
362
405
        if not getattr(lf, 'supports_diff', False):
363
406
            rqst['diff_type'] = None
 
407
        if not getattr(lf, 'supports_signatures', False):
 
408
            rqst['signature'] = False
364
409
 
365
410
        # Find and print the interesting revisions
366
411
        generator = self._generator_factory(self.branch, rqst)
367
 
        for lr in generator.iter_log_revisions():
368
 
            lf.log_revision(lr)
 
412
        try:
 
413
            for lr in generator.iter_log_revisions():
 
414
                lf.log_revision(lr)
 
415
        except errors.GhostRevisionUnusableHere:
 
416
            raise errors.BzrCommandError(
 
417
                    gettext('Further revision history missing.'))
369
418
        lf.show_advice()
370
419
 
371
420
    def _generator_factory(self, branch, rqst):
372
421
        """Make the LogGenerator object to use.
373
 
        
 
422
 
374
423
        Subclasses may wish to override this.
375
424
        """
376
425
        return _DefaultLogGenerator(branch, rqst)
400
449
        levels = rqst.get('levels')
401
450
        limit = rqst.get('limit')
402
451
        diff_type = rqst.get('diff_type')
 
452
        show_signature = rqst.get('signature')
 
453
        omit_merges = rqst.get('omit_merges')
403
454
        log_count = 0
404
455
        revision_iterator = self._create_log_revision_iterator()
405
456
        for revs in revision_iterator:
407
458
                # 0 levels means show everything; merge_depth counts from 0
408
459
                if levels != 0 and merge_depth >= levels:
409
460
                    continue
 
461
                if omit_merges and len(rev.parent_ids) > 1:
 
462
                    continue
 
463
                if rev is None:
 
464
                    raise errors.GhostRevisionUnusableHere(rev_id)
410
465
                if diff_type is None:
411
466
                    diff = None
412
467
                else:
413
468
                    diff = self._format_diff(rev, rev_id, diff_type)
 
469
                if show_signature:
 
470
                    signature = format_signature_validity(rev_id, self.branch)
 
471
                else:
 
472
                    signature = None
414
473
                yield LogRevision(rev, revno, merge_depth, delta,
415
 
                    self.rev_tag_dict.get(rev_id), diff)
 
474
                    self.rev_tag_dict.get(rev_id), diff, signature)
416
475
                if limit:
417
476
                    log_count += 1
418
477
                    if log_count >= limit:
431
490
            specific_files = [tree_2.id2path(id) for id in file_ids]
432
491
        else:
433
492
            specific_files = None
434
 
        s = StringIO()
 
493
        s = BytesIO()
 
494
        path_encoding = get_diff_header_encoding()
435
495
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
436
 
            new_label='')
 
496
            new_label='', path_encoding=path_encoding)
437
497
        return s.getvalue()
438
498
 
439
499
    def _create_log_revision_iterator(self):
472
532
 
473
533
        # Apply the other filters
474
534
        return make_log_rev_iterator(self.branch, view_revisions,
475
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
535
            rqst.get('delta_type'), rqst.get('match'),
476
536
            file_ids=rqst.get('specific_fileids'),
477
537
            direction=rqst.get('direction'))
478
538
 
491
551
            rqst.get('specific_fileids')[0], view_revisions,
492
552
            include_merges=rqst.get('levels') != 1)
493
553
        return make_log_rev_iterator(self.branch, view_revisions,
494
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
554
            rqst.get('delta_type'), rqst.get('match'))
495
555
 
496
556
 
497
557
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
565
             a list of the same tuples.
506
566
    """
507
567
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
 
        raise errors.BzrCommandError(
509
 
            '--exclude-common-ancestry requires two different revisions')
 
568
        raise errors.BzrCommandError(gettext(
 
569
            '--exclude-common-ancestry requires two different revisions'))
510
570
    if direction not in ('reverse', 'forward'):
511
 
        raise ValueError('invalid direction %r' % direction)
 
571
        raise ValueError(gettext('invalid direction %r') % direction)
512
572
    br_revno, br_rev_id = branch.last_revision_info()
513
573
    if br_revno == 0:
514
574
        return []
517
577
        and (not generate_merge_revisions
518
578
             or not _has_merges(branch, end_rev_id))):
519
579
        # If a single revision is requested, check we can handle it
520
 
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
521
 
                                           br_revno)
522
 
    elif not generate_merge_revisions:
523
 
        # If we only want to see linear revisions, we can iterate ...
524
 
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
525
 
                                             direction)
526
 
        if direction == 'forward':
527
 
            iter_revs = reversed(iter_revs)
528
 
    else:
529
 
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
530
 
                                            direction, delayed_graph_generation,
531
 
                                            exclude_common_ancestry)
532
 
        if direction == 'forward':
533
 
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
580
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
581
                                       br_revno)
 
582
    if not generate_merge_revisions:
 
583
        try:
 
584
            # If we only want to see linear revisions, we can iterate ...
 
585
            iter_revs = _linear_view_revisions(
 
586
                branch, start_rev_id, end_rev_id,
 
587
                exclude_common_ancestry=exclude_common_ancestry)
 
588
            # If a start limit was given and it's not obviously an
 
589
            # ancestor of the end limit, check it before outputting anything
 
590
            if (direction == 'forward'
 
591
                or (start_rev_id and not _is_obvious_ancestor(
 
592
                        branch, start_rev_id, end_rev_id))):
 
593
                    iter_revs = list(iter_revs)
 
594
            if direction == 'forward':
 
595
                iter_revs = reversed(iter_revs)
 
596
            return iter_revs
 
597
        except _StartNotLinearAncestor:
 
598
            # Switch to the slower implementation that may be able to find a
 
599
            # non-obvious ancestor out of the left-hand history.
 
600
            pass
 
601
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
602
                                        direction, delayed_graph_generation,
 
603
                                        exclude_common_ancestry)
 
604
    if direction == 'forward':
 
605
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
534
606
    return iter_revs
535
607
 
536
608
 
539
611
        # It's the tip
540
612
        return [(br_rev_id, br_revno, 0)]
541
613
    else:
542
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
543
 
        revno_str = '.'.join(str(n) for n in revno)
 
614
        revno_str = _compute_revno_str(branch, rev_id)
544
615
        return [(rev_id, revno_str, 0)]
545
616
 
546
617
 
547
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
548
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
549
 
    # If a start limit was given and it's not obviously an
550
 
    # ancestor of the end limit, check it before outputting anything
551
 
    if direction == 'forward' or (start_rev_id
552
 
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
553
 
        try:
554
 
            result = list(result)
555
 
        except _StartNotLinearAncestor:
556
 
            raise errors.BzrCommandError('Start revision not found in'
557
 
                ' left-hand history of end revision.')
558
 
    return result
559
 
 
560
 
 
561
618
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
619
                            delayed_graph_generation,
563
620
                            exclude_common_ancestry=False):
572
629
    if delayed_graph_generation:
573
630
        try:
574
631
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
632
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
633
                if _has_merges(branch, rev_id):
577
634
                    # The end_rev_id can be nested down somewhere. We need an
578
635
                    # explicit ancestry check. There is an ambiguity here as we
599
656
        except _StartNotLinearAncestor:
600
657
            # A merge was never detected so the lower revision limit can't
601
658
            # be nested down somewhere
602
 
            raise errors.BzrCommandError('Start revision not found in'
603
 
                ' history of end revision.')
 
659
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
660
                ' history of end revision.'))
604
661
 
605
662
    # We exit the loop above because we encounter a revision with merges, from
606
663
    # this revision, we need to switch to _graph_view_revisions.
610
667
    # shown naturally, i.e. just like it is for linear logging. We can easily
611
668
    # make forward the exact opposite display, but showing the merge revisions
612
669
    # indented at the end seems slightly nicer in that case.
613
 
    view_revisions = chain(iter(initial_revisions),
 
670
    view_revisions = itertools.chain(iter(initial_revisions),
614
671
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
615
672
                              rebase_initial_depths=(direction == 'reverse'),
616
673
                              exclude_common_ancestry=exclude_common_ancestry))
623
680
    return len(parents) > 1
624
681
 
625
682
 
 
683
def _compute_revno_str(branch, rev_id):
 
684
    """Compute the revno string from a rev_id.
 
685
 
 
686
    :return: The revno string, or None if the revision is not in the supplied
 
687
        branch.
 
688
    """
 
689
    try:
 
690
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
691
    except errors.NoSuchRevision:
 
692
        # The revision must be outside of this branch
 
693
        return None
 
694
    else:
 
695
        return '.'.join(str(n) for n in revno)
 
696
 
 
697
 
626
698
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
699
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
628
700
    if start_rev_id and end_rev_id:
629
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
630
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
701
        try:
 
702
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
703
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
704
        except errors.NoSuchRevision:
 
705
            # one or both is not in the branch; not obvious
 
706
            return False
631
707
        if len(start_dotted) == 1 and len(end_dotted) == 1:
632
708
            # both on mainline
633
709
            return start_dotted[0] <= end_dotted[0]
643
719
    return True
644
720
 
645
721
 
646
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
722
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
723
                           exclude_common_ancestry=False):
647
724
    """Calculate a sequence of revisions to view, newest to oldest.
648
725
 
649
726
    :param start_rev_id: the lower revision-id
650
727
    :param end_rev_id: the upper revision-id
 
728
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
729
        the iterated revisions.
651
730
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
731
        dotted_revno will be None for ghosts
652
732
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
 
      is not found walking the left-hand history
 
733
        is not found walking the left-hand history
654
734
    """
655
735
    br_revno, br_rev_id = branch.last_revision_info()
656
736
    repo = branch.repository
 
737
    graph = repo.get_graph()
657
738
    if start_rev_id is None and end_rev_id is None:
658
739
        cur_revno = br_revno
659
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
660
 
            yield revision_id, str(cur_revno), 0
661
 
            cur_revno -= 1
 
740
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
 
741
            (_mod_revision.NULL_REVISION,))
 
742
        while True:
 
743
            try:
 
744
                revision_id = next(graph_iter)
 
745
            except errors.RevisionNotPresent as e:
 
746
                # Oops, a ghost.
 
747
                yield e.revision_id, None, None
 
748
                break
 
749
            else:
 
750
                yield revision_id, str(cur_revno), 0
 
751
                cur_revno -= 1
662
752
    else:
663
753
        if end_rev_id is None:
664
754
            end_rev_id = br_rev_id
665
755
        found_start = start_rev_id is None
666
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
667
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
668
 
            revno_str = '.'.join(str(n) for n in revno)
669
 
            if not found_start and revision_id == start_rev_id:
670
 
                yield revision_id, revno_str, 0
671
 
                found_start = True
 
756
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
 
757
            (_mod_revision.NULL_REVISION,))
 
758
        while True:
 
759
            try:
 
760
                revision_id = next(graph_iter)
 
761
            except StopIteration:
 
762
                break
 
763
            except errors.RevisionNotPresent as e:
 
764
                # Oops, a ghost.
 
765
                yield e.revision_id, None, None
672
766
                break
673
767
            else:
674
 
                yield revision_id, revno_str, 0
675
 
        else:
676
 
            if not found_start:
677
 
                raise _StartNotLinearAncestor()
 
768
                revno_str = _compute_revno_str(branch, revision_id)
 
769
                if not found_start and revision_id == start_rev_id:
 
770
                    if not exclude_common_ancestry:
 
771
                        yield revision_id, revno_str, 0
 
772
                    found_start = True
 
773
                    break
 
774
                else:
 
775
                    yield revision_id, revno_str, 0
 
776
        if not found_start:
 
777
            raise _StartNotLinearAncestor()
678
778
 
679
779
 
680
780
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
721
821
            yield rev_id, '.'.join(map(str, revno)), merge_depth
722
822
 
723
823
 
724
 
@deprecated_function(deprecated_in((2, 2, 0)))
725
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
726
 
        specific_fileid, generate_merge_revisions):
727
 
    """Calculate the revisions to view.
728
 
 
729
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
730
 
             a list of the same tuples.
731
 
    """
732
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
733
 
        end_revision)
734
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
735
 
        direction, generate_merge_revisions or specific_fileid))
736
 
    if specific_fileid:
737
 
        view_revisions = _filter_revisions_touching_file_id(branch,
738
 
            specific_fileid, view_revisions,
739
 
            include_merges=generate_merge_revisions)
740
 
    return _rebase_merge_depth(view_revisions)
741
 
 
742
 
 
743
824
def _rebase_merge_depth(view_revisions):
744
825
    """Adjust depths upwards so the top level is 0."""
745
826
    # If either the first or last revision have a merge_depth of 0, we're done
746
827
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
747
 
        min_depth = min([d for r,n,d in view_revisions])
 
828
        min_depth = min([d for r, n, d in view_revisions])
748
829
        if min_depth != 0:
749
 
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
 
830
            view_revisions = [(r, n, d-min_depth) for r, n, d in view_revisions]
750
831
    return view_revisions
751
832
 
752
833
 
767
848
    """
768
849
    # Convert view_revisions into (view, None, None) groups to fit with
769
850
    # the standard interface here.
770
 
    if type(view_revisions) == list:
 
851
    if isinstance(view_revisions, list):
771
852
        # A single batch conversion is faster than many incremental ones.
772
853
        # As we have all the data, do a batch conversion.
773
854
        nones = [None] * len(view_revisions)
774
 
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
 
855
        log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
775
856
    else:
776
857
        def _convert():
777
858
            for view in view_revisions:
789
870
    return log_rev_iterator
790
871
 
791
872
 
792
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
873
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
793
874
    """Create a filtered iterator of log_rev_iterator matching on a regex.
794
875
 
795
876
    :param branch: The branch being logged.
796
877
    :param generate_delta: Whether to generate a delta for each revision.
797
 
    :param search: A user text search string.
 
878
    :param match: A dictionary with properties as keys and lists of strings
 
879
        as values. To match, a revision may match any of the supplied strings
 
880
        within a single property but must match at least one string for each
 
881
        property.
798
882
    :param log_rev_iterator: An input iterator containing all revisions that
799
883
        could be displayed, in lists.
800
884
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
801
885
        delta).
802
886
    """
803
 
    if search is None:
 
887
    if not match:
804
888
        return log_rev_iterator
805
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
806
 
            'log message filter')
807
 
    return _filter_message_re(searchRE, log_rev_iterator)
808
 
 
809
 
 
810
 
def _filter_message_re(searchRE, log_rev_iterator):
 
889
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
890
                for k, v in match.items()]
 
891
    return _filter_re(searchRE, log_rev_iterator)
 
892
 
 
893
 
 
894
def _filter_re(searchRE, log_rev_iterator):
811
895
    for revs in log_rev_iterator:
812
 
        new_revs = []
813
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
814
 
            if searchRE.search(rev.message):
815
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
816
 
        yield new_revs
817
 
 
 
896
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
897
        if new_revs:
 
898
            yield new_revs
 
899
 
 
900
def _match_filter(searchRE, rev):
 
901
    strings = {
 
902
               'message': (rev.message,),
 
903
               'committer': (rev.committer,),
 
904
               'author': (rev.get_apparent_authors()),
 
905
               'bugs': list(rev.iter_bugs())
 
906
               }
 
907
    strings[''] = [item for inner_list in strings.values()
 
908
                   for item in inner_list]
 
909
    for (k, v) in searchRE:
 
910
        if k in strings and not _match_any_filter(strings[k], v):
 
911
            return False
 
912
    return True
 
913
 
 
914
def _match_any_filter(strings, res):
 
915
    return any(re.search(s) for re in res for s in strings)
818
916
 
819
917
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
820
918
    fileids=None, direction='reverse'):
865
963
        new_revs = []
866
964
        if delta_type == 'full' and not check_fileids:
867
965
            deltas = repository.get_deltas_for_revisions(revisions)
868
 
            for rev, delta in izip(revs, deltas):
 
966
            for rev, delta in zip(revs, deltas):
869
967
                new_revs.append((rev[0], rev[1], delta))
870
968
        else:
871
969
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
872
 
            for rev, delta in izip(revs, deltas):
 
970
            for rev, delta in zip(revs, deltas):
873
971
                if check_fileids:
874
972
                    if delta is None or not delta.has_changed():
875
973
                        continue
893
991
 
894
992
def _update_fileids(delta, fileids, stop_on):
895
993
    """Update the set of file-ids to search based on file lifecycle events.
896
 
    
 
994
 
897
995
    :param fileids: a set of fileids to update
898
996
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
899
997
      fileids set once their add or remove entry is detected respectively
923
1021
    for revs in log_rev_iterator:
924
1022
        # r = revision_id, n = revno, d = merge depth
925
1023
        revision_ids = [view[0] for view, _, _ in revs]
926
 
        revisions = repository.get_revisions(revision_ids)
927
 
        revs = [(rev[0], revision, rev[2]) for rev, revision in
928
 
            izip(revs, revisions)]
929
 
        yield revs
 
1024
        revisions = dict(repository.iter_revisions(revision_ids))
 
1025
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
930
1026
 
931
1027
 
932
1028
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
940
1036
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
941
1037
        delta).
942
1038
    """
943
 
    repository = branch.repository
944
1039
    num = 9
945
1040
    for batch in log_rev_iterator:
946
1041
        batch = iter(batch)
995
1090
    if branch_revno != 0:
996
1091
        if (start_rev_id == _mod_revision.NULL_REVISION
997
1092
            or end_rev_id == _mod_revision.NULL_REVISION):
998
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1093
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
999
1094
        if start_revno > end_revno:
1000
 
            raise errors.BzrCommandError("Start revision must be older than "
1001
 
                                         "the end revision.")
 
1095
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1096
                                         "older than the end revision."))
1002
1097
    return (start_rev_id, end_rev_id)
1003
1098
 
1004
1099
 
1053
1148
 
1054
1149
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1055
1150
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1056
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1151
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1057
1152
    if start_revno > end_revno:
1058
 
        raise errors.BzrCommandError("Start revision must be older than "
1059
 
                                     "the end revision.")
 
1153
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1154
                                     "than the end revision."))
1060
1155
 
1061
1156
    if end_revno < start_revno:
1062
1157
        return None, None, None, None
1063
1158
    cur_revno = branch_revno
1064
1159
    rev_nos = {}
1065
1160
    mainline_revs = []
1066
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1067
 
                        branch_last_revision):
 
1161
    graph = branch.repository.get_graph()
 
1162
    for revision_id in graph.iter_lefthand_ancestry(
 
1163
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1164
        if cur_revno < start_revno:
1069
1165
            # We have gone far enough, but we always add 1 more revision
1070
1166
            rev_nos[revision_id] = cur_revno
1084
1180
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1085
1181
 
1086
1182
 
1087
 
@deprecated_function(deprecated_in((2, 2, 0)))
1088
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1089
 
    """Filter view_revisions based on revision ranges.
1090
 
 
1091
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1092
 
            tuples to be filtered.
1093
 
 
1094
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1095
 
            If NONE then all revisions up to the end_rev_id are logged.
1096
 
 
1097
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1098
 
            If NONE then all revisions up to the end of the log are logged.
1099
 
 
1100
 
    :return: The filtered view_revisions.
1101
 
    """
1102
 
    if start_rev_id or end_rev_id:
1103
 
        revision_ids = [r for r, n, d in view_revisions]
1104
 
        if start_rev_id:
1105
 
            start_index = revision_ids.index(start_rev_id)
1106
 
        else:
1107
 
            start_index = 0
1108
 
        if start_rev_id == end_rev_id:
1109
 
            end_index = start_index
1110
 
        else:
1111
 
            if end_rev_id:
1112
 
                end_index = revision_ids.index(end_rev_id)
1113
 
            else:
1114
 
                end_index = len(view_revisions) - 1
1115
 
        # To include the revisions merged into the last revision,
1116
 
        # extend end_rev_id down to, but not including, the next rev
1117
 
        # with the same or lesser merge_depth
1118
 
        end_merge_depth = view_revisions[end_index][2]
1119
 
        try:
1120
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1121
 
                if view_revisions[index][2] <= end_merge_depth:
1122
 
                    end_index = index - 1
1123
 
                    break
1124
 
        except IndexError:
1125
 
            # if the search falls off the end then log to the end as well
1126
 
            end_index = len(view_revisions) - 1
1127
 
        view_revisions = view_revisions[start_index:end_index+1]
1128
 
    return view_revisions
1129
 
 
1130
 
 
1131
1183
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1132
1184
    include_merges=True):
1133
1185
    r"""Return the list of revision ids which touch a given file id.
1136
1188
    This includes the revisions which directly change the file id,
1137
1189
    and the revisions which merge these changes. So if the
1138
1190
    revision graph is::
 
1191
 
1139
1192
        A-.
1140
1193
        |\ \
1141
1194
        B C E
1168
1221
    """
1169
1222
    # Lookup all possible text keys to determine which ones actually modified
1170
1223
    # the file.
 
1224
    graph = branch.repository.get_file_graph()
 
1225
    get_parent_map = graph.get_parent_map
1171
1226
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1227
    next_keys = None
1173
1228
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1232
    #       indexing layer. We might consider passing in hints as to the known
1178
1233
    #       access pattern (sparse/clustered, high success rate/low success
1179
1234
    #       rate). This particular access is clustered with a low success rate.
1180
 
    get_parent_map = branch.repository.texts.get_parent_map
1181
1235
    modified_text_revisions = set()
1182
1236
    chunk_size = 1000
1183
 
    for start in xrange(0, len(text_keys), chunk_size):
 
1237
    for start in range(0, len(text_keys), chunk_size):
1184
1238
        next_keys = text_keys[start:start + chunk_size]
1185
1239
        # Only keep the revision_id portion of the key
1186
1240
        modified_text_revisions.update(
1201
1255
 
1202
1256
        if rev_id in modified_text_revisions:
1203
1257
            # This needs to be logged, along with the extra revisions
1204
 
            for idx in xrange(len(current_merge_stack)):
 
1258
            for idx in range(len(current_merge_stack)):
1205
1259
                node = current_merge_stack[idx]
1206
1260
                if node is not None:
1207
1261
                    if include_merges or node[2] == 0:
1210
1264
    return result
1211
1265
 
1212
1266
 
1213
 
@deprecated_function(deprecated_in((2, 2, 0)))
1214
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1215
 
                       include_merges=True):
1216
 
    """Produce an iterator of revisions to show
1217
 
    :return: an iterator of (revision_id, revno, merge_depth)
1218
 
    (if there is no revno for a revision, None is supplied)
1219
 
    """
1220
 
    if not include_merges:
1221
 
        revision_ids = mainline_revs[1:]
1222
 
        if direction == 'reverse':
1223
 
            revision_ids.reverse()
1224
 
        for revision_id in revision_ids:
1225
 
            yield revision_id, str(rev_nos[revision_id]), 0
1226
 
        return
1227
 
    graph = branch.repository.get_graph()
1228
 
    # This asks for all mainline revisions, which means we only have to spider
1229
 
    # sideways, rather than depth history. That said, its still size-of-history
1230
 
    # and should be addressed.
1231
 
    # mainline_revisions always includes an extra revision at the beginning, so
1232
 
    # don't request it.
1233
 
    parent_map = dict(((key, value) for key, value in
1234
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1235
 
    # filter out ghosts; merge_sort errors on ghosts.
1236
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1237
 
    merge_sorted_revisions = tsort.merge_sort(
1238
 
        rev_graph,
1239
 
        mainline_revs[-1],
1240
 
        mainline_revs,
1241
 
        generate_revno=True)
1242
 
 
1243
 
    if direction == 'forward':
1244
 
        # forward means oldest first.
1245
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1246
 
    elif direction != 'reverse':
1247
 
        raise ValueError('invalid direction %r' % direction)
1248
 
 
1249
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1250
 
         ) in merge_sorted_revisions:
1251
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1252
 
 
1253
 
 
1254
1267
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1255
1268
    """Reverse revisions by depth.
1256
1269
 
1291
1304
    """
1292
1305
 
1293
1306
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
 
                 tags=None, diff=None):
 
1307
                 tags=None, diff=None, signature=None):
1295
1308
        self.rev = rev
1296
 
        self.revno = str(revno)
 
1309
        if revno is None:
 
1310
            self.revno = None
 
1311
        else:
 
1312
            self.revno = str(revno)
1297
1313
        self.merge_depth = merge_depth
1298
1314
        self.delta = delta
1299
1315
        self.tags = tags
1300
1316
        self.diff = diff
 
1317
        self.signature = signature
1301
1318
 
1302
1319
 
1303
1320
class LogFormatter(object):
1312
1329
    to indicate which LogRevision attributes it supports:
1313
1330
 
1314
1331
    - supports_delta must be True if this log formatter supports delta.
1315
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1316
 
        attribute describes whether the 'short_status' format (1) or the long
1317
 
        one (2) should be used.
 
1332
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1333
      attribute describes whether the 'short_status' format (1) or the long
 
1334
      one (2) should be used.
1318
1335
 
1319
1336
    - supports_merge_revisions must be True if this log formatter supports
1320
 
        merge revisions.  If not, then only mainline revisions will be passed
1321
 
        to the formatter.
 
1337
      merge revisions.  If not, then only mainline revisions will be passed
 
1338
      to the formatter.
1322
1339
 
1323
1340
    - preferred_levels is the number of levels this formatter defaults to.
1324
 
        The default value is zero meaning display all levels.
1325
 
        This value is only relevant if supports_merge_revisions is True.
 
1341
      The default value is zero meaning display all levels.
 
1342
      This value is only relevant if supports_merge_revisions is True.
1326
1343
 
1327
1344
    - supports_tags must be True if this log formatter supports tags.
1328
 
        Otherwise the tags attribute may not be populated.
 
1345
      Otherwise the tags attribute may not be populated.
1329
1346
 
1330
1347
    - supports_diff must be True if this log formatter supports diffs.
1331
 
        Otherwise the diff attribute may not be populated.
 
1348
      Otherwise the diff attribute may not be populated.
 
1349
 
 
1350
    - supports_signatures must be True if this log formatter supports GPG
 
1351
      signatures.
1332
1352
 
1333
1353
    Plugins can register functions to show custom revision properties using
1334
1354
    the properties_handler_registry. The registered function
1335
 
    must respect the following interface description:
 
1355
    must respect the following interface description::
 
1356
 
1336
1357
        def my_show_properties(properties_dict):
1337
1358
            # code that returns a dict {'name':'value'} of the properties
1338
1359
            # to be shown
1341
1362
 
1342
1363
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1364
                 delta_format=None, levels=None, show_advice=False,
1344
 
                 to_exact_file=None):
 
1365
                 to_exact_file=None, author_list_handler=None):
1345
1366
        """Create a LogFormatter.
1346
1367
 
1347
1368
        :param to_file: the file to output to
1348
 
        :param to_exact_file: if set, gives an output stream to which 
 
1369
        :param to_exact_file: if set, gives an output stream to which
1349
1370
             non-Unicode diffs are written.
1350
1371
        :param show_ids: if True, revision-ids are to be displayed
1351
1372
        :param show_timezone: the timezone to use
1355
1376
          let the log formatter decide.
1356
1377
        :param show_advice: whether to show advice at the end of the
1357
1378
          log or not
 
1379
        :param author_list_handler: callable generating a list of
 
1380
          authors to display for a given revision
1358
1381
        """
1359
1382
        self.to_file = to_file
1360
1383
        # 'exact' stream used to show diff, it should print content 'as is'
1375
1398
        self.levels = levels
1376
1399
        self._show_advice = show_advice
1377
1400
        self._merge_count = 0
 
1401
        self._author_list_handler = author_list_handler
1378
1402
 
1379
1403
    def get_levels(self):
1380
1404
        """Get the number of levels to display or 0 for all."""
1399
1423
            if advice_sep:
1400
1424
                self.to_file.write(advice_sep)
1401
1425
            self.to_file.write(
1402
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1426
                "Use --include-merged or -n0 to see merged revisions.\n")
1403
1427
 
1404
1428
    def get_advice_separator(self):
1405
1429
        """Get the text separating the log from the closing advice."""
1412
1436
        return address
1413
1437
 
1414
1438
    def short_author(self, rev):
1415
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1416
 
        if name:
1417
 
            return name
1418
 
        return address
 
1439
        return self.authors(rev, 'first', short=True, sep=', ')
 
1440
 
 
1441
    def authors(self, rev, who, short=False, sep=None):
 
1442
        """Generate list of authors, taking --authors option into account.
 
1443
 
 
1444
        The caller has to specify the name of a author list handler,
 
1445
        as provided by the author list registry, using the ``who``
 
1446
        argument.  That name only sets a default, though: when the
 
1447
        user selected a different author list generation using the
 
1448
        ``--authors`` command line switch, as represented by the
 
1449
        ``author_list_handler`` constructor argument, that value takes
 
1450
        precedence.
 
1451
 
 
1452
        :param rev: The revision for which to generate the list of authors.
 
1453
        :param who: Name of the default handler.
 
1454
        :param short: Whether to shorten names to either name or address.
 
1455
        :param sep: What separator to use for automatic concatenation.
 
1456
        """
 
1457
        if self._author_list_handler is not None:
 
1458
            # The user did specify --authors, which overrides the default
 
1459
            author_list_handler = self._author_list_handler
 
1460
        else:
 
1461
            # The user didn't specify --authors, so we use the caller's default
 
1462
            author_list_handler = author_list_registry.get(who)
 
1463
        names = author_list_handler(rev)
 
1464
        if short:
 
1465
            for i in range(len(names)):
 
1466
                name, address = config.parse_username(names[i])
 
1467
                if name:
 
1468
                    names[i] = name
 
1469
                else:
 
1470
                    names[i] = address
 
1471
        if sep is not None:
 
1472
            names = sep.join(names)
 
1473
        return names
1419
1474
 
1420
1475
    def merge_marker(self, revision):
1421
1476
        """Get the merge marker to include in the output or '' if none."""
1491
1546
    supports_delta = True
1492
1547
    supports_tags = True
1493
1548
    supports_diff = True
 
1549
    supports_signatures = True
1494
1550
 
1495
1551
    def __init__(self, *args, **kwargs):
1496
1552
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1516
1572
                self.merge_marker(revision)))
1517
1573
        if revision.tags:
1518
1574
            lines.append('tags: %s' % (', '.join(revision.tags)))
1519
 
        if self.show_ids:
 
1575
        if self.show_ids or revision.revno is None:
1520
1576
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1577
        if self.show_ids:
1521
1578
            for parent_id in revision.rev.parent_ids:
1522
1579
                lines.append('parent: %s' % (parent_id,))
1523
1580
        lines.extend(self.custom_properties(revision.rev))
1524
1581
 
1525
1582
        committer = revision.rev.committer
1526
 
        authors = revision.rev.get_apparent_authors()
 
1583
        authors = self.authors(revision.rev, 'all')
1527
1584
        if authors != [committer]:
1528
1585
            lines.append('author: %s' % (", ".join(authors),))
1529
1586
        lines.append('committer: %s' % (committer,))
1534
1591
 
1535
1592
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1536
1593
 
 
1594
        if revision.signature is not None:
 
1595
            lines.append('signature: ' + revision.signature)
 
1596
 
1537
1597
        lines.append('message:')
1538
1598
        if not revision.rev.message:
1539
1599
            lines.append('  (no message)')
1547
1607
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1548
1608
        if revision.delta is not None:
1549
1609
            # Use the standard status output to display changes
1550
 
            from bzrlib.delta import report_delta
1551
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1610
            from breezy.delta import report_delta
 
1611
            report_delta(to_file, revision.delta, short_status=False,
1552
1612
                         show_ids=self.show_ids, indent=indent)
1553
1613
        if revision.diff is not None:
1554
1614
            to_file.write(indent + 'diff:\n')
1586
1646
        indent = '    ' * depth
1587
1647
        revno_width = self.revno_width_by_depth.get(depth)
1588
1648
        if revno_width is None:
1589
 
            if revision.revno.find('.') == -1:
 
1649
            if revision.revno is None or revision.revno.find('.') == -1:
1590
1650
                # mainline revno, e.g. 12345
1591
1651
                revno_width = 5
1592
1652
            else:
1600
1660
        if revision.tags:
1601
1661
            tags = ' {%s}' % (', '.join(revision.tags))
1602
1662
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
 
                revision.revno, self.short_author(revision.rev),
 
1663
                revision.revno or "", self.short_author(revision.rev),
1604
1664
                format_date(revision.rev.timestamp,
1605
1665
                            revision.rev.timezone or 0,
1606
1666
                            self.show_timezone, date_fmt="%Y-%m-%d",
1607
1667
                            show_offset=False),
1608
1668
                tags, self.merge_marker(revision)))
1609
1669
        self.show_properties(revision.rev, indent+offset)
1610
 
        if self.show_ids:
 
1670
        if self.show_ids or revision.revno is None:
1611
1671
            to_file.write(indent + offset + 'revision-id:%s\n'
1612
1672
                          % (revision.rev.revision_id,))
1613
1673
        if not revision.rev.message:
1619
1679
 
1620
1680
        if revision.delta is not None:
1621
1681
            # Use the standard status output to display changes
1622
 
            from bzrlib.delta import report_delta
1623
 
            report_delta(to_file, revision.delta, 
1624
 
                         short_status=self.delta_format==1, 
 
1682
            from breezy.delta import report_delta
 
1683
            report_delta(to_file, revision.delta,
 
1684
                         short_status=self.delta_format==1,
1625
1685
                         show_ids=self.show_ids, indent=indent + offset)
1626
1686
        if revision.diff is not None:
1627
1687
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1666
1726
 
1667
1727
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1728
        """Format log info into one string. Truncate tail of string
1669
 
        :param  revno:      revision number or None.
1670
 
                            Revision numbers counts from 1.
1671
 
        :param  rev:        revision object
1672
 
        :param  max_chars:  maximum length of resulting string
1673
 
        :param  tags:       list of tags or None
1674
 
        :param  prefix:     string to prefix each line
1675
 
        :return:            formatted truncated string
 
1729
 
 
1730
        :param revno:      revision number or None.
 
1731
                           Revision numbers counts from 1.
 
1732
        :param rev:        revision object
 
1733
        :param max_chars:  maximum length of resulting string
 
1734
        :param tags:       list of tags or None
 
1735
        :param prefix:     string to prefix each line
 
1736
        :return:           formatted truncated string
1676
1737
        """
1677
1738
        out = []
1678
1739
        if revno:
1679
1740
            # show revno only when is not None
1680
1741
            out.append("%s:" % revno)
1681
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1742
        if max_chars is not None:
 
1743
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1744
        else:
 
1745
            out.append(self.short_author(rev))
1682
1746
        out.append(self.date_string(rev))
1683
1747
        if len(rev.parent_ids) > 1:
1684
1748
            out.append('[merge]')
1703
1767
                               self.show_timezone,
1704
1768
                               date_fmt='%Y-%m-%d',
1705
1769
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
1707
 
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
 
1770
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1771
        committer_str = committer_str.replace(' <', '  <')
 
1772
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
1708
1773
 
1709
1774
        if revision.delta is not None and revision.delta.has_changed():
1710
1775
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1711
1776
                path, = c[:1]
1712
1777
                to_file.write('\t* %s:\n' % (path,))
1713
1778
            for c in revision.delta.renamed:
1714
 
                oldpath,newpath = c[:2]
 
1779
                oldpath, newpath = c[:2]
1715
1780
                # For renamed files, show both the old and the new path
1716
 
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
 
1781
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
1717
1782
            to_file.write('\n')
1718
1783
 
1719
1784
        if not revision.rev.message:
1742
1807
        return self.get(name)(*args, **kwargs)
1743
1808
 
1744
1809
    def get_default(self, branch):
1745
 
        return self.get(branch.get_config().log_format())
 
1810
        c = branch.get_config_stack()
 
1811
        return self.get(c.get('log_format'))
1746
1812
 
1747
1813
 
1748
1814
log_formatter_registry = LogFormatterRegistry()
1749
1815
 
1750
1816
 
1751
1817
log_formatter_registry.register('short', ShortLogFormatter,
1752
 
                                'Moderately short log format')
 
1818
                                'Moderately short log format.')
1753
1819
log_formatter_registry.register('long', LongLogFormatter,
1754
 
                                'Detailed log format')
 
1820
                                'Detailed log format.')
1755
1821
log_formatter_registry.register('line', LineLogFormatter,
1756
 
                                'Log format with one line per revision')
 
1822
                                'Log format with one line per revision.')
1757
1823
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1758
 
                                'Format used by GNU ChangeLog files')
 
1824
                                'Format used by GNU ChangeLog files.')
1759
1825
 
1760
1826
 
1761
1827
def register_formatter(name, formatter):
1771
1837
    try:
1772
1838
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1773
1839
    except KeyError:
1774
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1775
 
 
1776
 
 
1777
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
 
    # deprecated; for compatibility
1779
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1780
 
    lf.show(revno, rev, delta)
 
1840
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1841
 
 
1842
 
 
1843
def author_list_all(rev):
 
1844
    return rev.get_apparent_authors()[:]
 
1845
 
 
1846
 
 
1847
def author_list_first(rev):
 
1848
    lst = rev.get_apparent_authors()
 
1849
    try:
 
1850
        return [lst[0]]
 
1851
    except IndexError:
 
1852
        return []
 
1853
 
 
1854
 
 
1855
def author_list_committer(rev):
 
1856
    return [rev.committer]
 
1857
 
 
1858
 
 
1859
author_list_registry = registry.Registry()
 
1860
 
 
1861
author_list_registry.register('all', author_list_all,
 
1862
                              'All authors')
 
1863
 
 
1864
author_list_registry.register('first', author_list_first,
 
1865
                              'The first author')
 
1866
 
 
1867
author_list_registry.register('committer', author_list_committer,
 
1868
                              'The committer')
1781
1869
 
1782
1870
 
1783
1871
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1800
1888
    # This is the first index which is different between
1801
1889
    # old and new
1802
1890
    base_idx = None
1803
 
    for i in xrange(max(len(new_rh),
1804
 
                        len(old_rh))):
 
1891
    for i in range(max(len(new_rh), len(old_rh))):
1805
1892
        if (len(new_rh) <= i
1806
1893
            or len(old_rh) <= i
1807
1894
            or new_rh[i] != old_rh[i]):
1848
1935
    old_revisions = set()
1849
1936
    new_history = []
1850
1937
    new_revisions = set()
1851
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1852
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1938
    graph = repository.get_graph()
 
1939
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1940
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1853
1941
    stop_revision = None
1854
1942
    do_old = True
1855
1943
    do_new = True
1856
1944
    while do_new or do_old:
1857
1945
        if do_new:
1858
1946
            try:
1859
 
                new_revision = new_iter.next()
 
1947
                new_revision = next(new_iter)
1860
1948
            except StopIteration:
1861
1949
                do_new = False
1862
1950
            else:
1867
1955
                    break
1868
1956
        if do_old:
1869
1957
            try:
1870
 
                old_revision = old_iter.next()
 
1958
                old_revision = next(old_iter)
1871
1959
            except StopIteration:
1872
1960
                do_old = False
1873
1961
            else:
1930
2018
        lf.log_revision(lr)
1931
2019
 
1932
2020
 
1933
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2021
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2022
    """Find file-ids and kinds given a list of files and a revision range.
1935
2023
 
1936
2024
    We search for files at the end of the range. If not found there,
1940
2028
    :param file_list: the list of paths given on the command line;
1941
2029
      the first of these can be a branch location or a file path,
1942
2030
      the remainder must be file paths
 
2031
    :param add_cleanup: When the branch returned is read locked,
 
2032
      an unlock call will be queued to the cleanup.
1943
2033
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2034
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
2035
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2036
      branch will be read-locked.
1947
2037
    """
1948
 
    from builtins import _get_revision_range, safe_relpath_files
1949
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
 
2038
    from breezy.builtins import _get_revision_range
 
2039
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2040
        file_list[0])
 
2041
    add_cleanup(b.lock_read().unlock)
1951
2042
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
2043
    # those paths might be deleted ones, they might be on a case-insensitive
1953
2044
    # filesystem and/or they might be in silly locations (like another branch).
1957
2048
    # case of running log in a nested directory, assuming paths beyond the
1958
2049
    # first one haven't been deleted ...
1959
2050
    if tree:
1960
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2051
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1961
2052
    else:
1962
2053
        relpaths = [path] + file_list[1:]
1963
2054
    info_list = []
1971
2062
        tree1 = None
1972
2063
        for fp in relpaths:
1973
2064
            file_id = tree.path2id(fp)
1974
 
            kind = _get_kind_for_file_id(tree, file_id)
 
2065
            kind = _get_kind_for_file_id(tree, fp, file_id)
1975
2066
            if file_id is None:
1976
2067
                # go back to when time began
1977
2068
                if tree1 is None:
1985
2076
                        tree1 = b.repository.revision_tree(rev1)
1986
2077
                if tree1:
1987
2078
                    file_id = tree1.path2id(fp)
1988
 
                    kind = _get_kind_for_file_id(tree1, file_id)
 
2079
                    kind = _get_kind_for_file_id(tree1, fp, file_id)
1989
2080
            info_list.append((fp, file_id, kind))
1990
2081
 
1991
2082
    elif start_rev_info == end_rev_info:
1993
2084
        tree = b.repository.revision_tree(end_rev_info.rev_id)
1994
2085
        for fp in relpaths:
1995
2086
            file_id = tree.path2id(fp)
1996
 
            kind = _get_kind_for_file_id(tree, file_id)
 
2087
            kind = _get_kind_for_file_id(tree, fp, file_id)
1997
2088
            info_list.append((fp, file_id, kind))
1998
2089
 
1999
2090
    else:
2007
2098
        tree1 = None
2008
2099
        for fp in relpaths:
2009
2100
            file_id = tree.path2id(fp)
2010
 
            kind = _get_kind_for_file_id(tree, file_id)
 
2101
            kind = _get_kind_for_file_id(tree, fp, file_id)
2011
2102
            if file_id is None:
2012
2103
                if tree1 is None:
2013
2104
                    rev_id = start_rev_info.rev_id
2017
2108
                    else:
2018
2109
                        tree1 = b.repository.revision_tree(rev_id)
2019
2110
                file_id = tree1.path2id(fp)
2020
 
                kind = _get_kind_for_file_id(tree1, file_id)
 
2111
                kind = _get_kind_for_file_id(tree1, fp, file_id)
2021
2112
            info_list.append((fp, file_id, kind))
2022
2113
    return b, info_list, start_rev_info, end_rev_info
2023
2114
 
2024
2115
 
2025
 
def _get_kind_for_file_id(tree, file_id):
 
2116
def _get_kind_for_file_id(tree, path, file_id):
2026
2117
    """Return the kind of a file-id or None if it doesn't exist."""
2027
2118
    if file_id is not None:
2028
 
        return tree.kind(file_id)
 
2119
        return tree.kind(path, file_id)
2029
2120
    else:
2030
2121
        return None
2031
2122
 
2034
2125
 
2035
2126
# Use the properties handlers to print out bug information if available
2036
2127
def _bugs_properties_handler(revision):
2037
 
    if revision.properties.has_key('bugs'):
 
2128
    if 'bugs' in revision.properties:
2038
2129
        bug_lines = revision.properties['bugs'].split('\n')
2039
2130
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2040
2131
        fixed_bug_urls = [row[0] for row in bug_rows if
2041
2132
                          len(row) > 1 and row[1] == 'fixed']
2042
2133
 
2043
2134
        if fixed_bug_urls:
2044
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2135
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2136
                    ' '.join(fixed_bug_urls)}
2045
2137
    return {}
2046
2138
 
2047
2139
properties_handler_registry.register('bugs_properties_handler',