/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: 2018-03-10 13:18:10 UTC
  • mto: This revision was merged to the branch mainline in revision 6893.
  • Revision ID: jelmer@jelmer.uk-20180310131810-iiblggbkb757eopm
Avoid call to has_id.

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