~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Patch Queue Manager
  • Date: 2016-02-01 19:56:05 UTC
  • mfrom: (6615.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20160201195605-o7rl92wf6uyum3fk
(vila) Open trunk again as 2.8b1 (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
 
18
 
 
19
17
"""Code to show logs of changes.
20
18
 
21
19
Various flavors of log can be produced:
49
47
all the changes since the previous revision that touched hello.c.
50
48
"""
51
49
 
 
50
from __future__ import absolute_import
 
51
 
52
52
import codecs
53
53
from cStringIO import StringIO
54
54
from itertools import (
65
65
lazy_import(globals(), """
66
66
 
67
67
from bzrlib import (
68
 
    bzrdir,
69
68
    config,
 
69
    controldir,
70
70
    diff,
71
71
    errors,
72
72
    foreign,
73
 
    osutils,
74
73
    repository as _mod_repository,
75
74
    revision as _mod_revision,
76
75
    revisionspec,
77
 
    trace,
78
76
    tsort,
79
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
80
79
""")
81
80
 
82
81
from bzrlib import (
 
82
    lazy_regex,
83
83
    registry,
84
84
    )
85
85
from bzrlib.osutils import (
86
86
    format_date,
87
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
88
89
    get_terminal_encoding,
89
90
    terminal_width,
90
91
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
94
 
    )
95
92
 
96
93
 
97
94
def find_touching_revisions(branch, file_id):
108
105
    last_ie = None
109
106
    last_path = None
110
107
    revno = 1
111
 
    for revision_id in branch.revision_history():
 
108
    graph = branch.repository.get_graph()
 
109
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
110
        [_mod_revision.NULL_REVISION]))
 
111
    for revision_id in reversed(history):
112
112
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if file_id in this_inv:
 
113
        if this_inv.has_id(file_id):
114
114
            this_ie = this_inv[file_id]
115
115
            this_path = this_inv.id2path(file_id)
116
116
        else:
138
138
        revno += 1
139
139
 
140
140
 
141
 
def _enumerate_history(branch):
142
 
    rh = []
143
 
    revno = 1
144
 
    for rev_id in branch.revision_history():
145
 
        rh.append((revno, rev_id))
146
 
        revno += 1
147
 
    return rh
148
 
 
149
 
 
150
141
def show_log(branch,
151
142
             lf,
152
143
             specific_fileid=None,
156
147
             end_revision=None,
157
148
             search=None,
158
149
             limit=None,
159
 
             show_diff=False):
 
150
             show_diff=False,
 
151
             match=None):
160
152
    """Write out human-readable log of commits to this branch.
161
153
 
162
154
    This function is being retained for backwards compatibility but
185
177
        if None or 0.
186
178
 
187
179
    :param show_diff: If True, output a diff after each revision.
 
180
 
 
181
    :param match: Dictionary of search lists to use when matching revision
 
182
      properties.
188
183
    """
189
184
    # Convert old-style parameters to new-style parameters
190
185
    if specific_fileid is not None:
214
209
    Logger(branch, rqst).show(lf)
215
210
 
216
211
 
217
 
# Note: This needs to be kept this in sync with the defaults in
 
212
# Note: This needs to be kept in sync with the defaults in
218
213
# make_log_request_dict() below
219
214
_DEFAULT_REQUEST_PARAMS = {
220
215
    'direction': 'reverse',
221
 
    'levels': 1,
 
216
    'levels': None,
222
217
    'generate_tags': True,
223
218
    'exclude_common_ancestry': False,
224
219
    '_match_using_deltas': True,
227
222
 
228
223
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
224
                          start_revision=None, end_revision=None, limit=None,
230
 
                          message_search=None, levels=1, generate_tags=True,
 
225
                          message_search=None, levels=None, generate_tags=True,
231
226
                          delta_type=None,
232
227
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
 
228
                          exclude_common_ancestry=False, match=None,
 
229
                          signature=False, omit_merges=False,
234
230
                          ):
235
231
    """Convenience function for making a logging request dictionary.
236
232
 
257
253
      matching commit messages
258
254
 
259
255
    :param levels: the number of levels of revisions to
260
 
      generate; 1 for just the mainline; 0 for all levels.
 
256
      generate; 1 for just the mainline; 0 for all levels, or None for
 
257
      a sensible default.
261
258
 
262
259
    :param generate_tags: If True, include tags for matched revisions.
263
 
 
 
260
`
264
261
    :param delta_type: Either 'full', 'partial' or None.
265
262
      'full' means generate the complete delta - adds/deletes/modifies/etc;
266
263
      'partial' means filter the delta using specific_fileids;
278
275
 
279
276
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
277
      range operator or as a graph difference.
 
278
 
 
279
    :param signature: show digital signature information
 
280
 
 
281
    :param match: Dictionary of list of search strings to use when filtering
 
282
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
283
      the empty string to match any of the preceding properties.
 
284
 
 
285
    :param omit_merges: If True, commits with more than one parent are
 
286
      omitted.
 
287
 
281
288
    """
 
289
    # Take care of old style message_search parameter
 
290
    if message_search:
 
291
        if match:
 
292
            if 'message' in match:
 
293
                match['message'].append(message_search)
 
294
            else:
 
295
                match['message'] = [message_search]
 
296
        else:
 
297
            match={ 'message': [message_search] }
282
298
    return {
283
299
        'direction': direction,
284
300
        'specific_fileids': specific_fileids,
285
301
        'start_revision': start_revision,
286
302
        'end_revision': end_revision,
287
303
        'limit': limit,
288
 
        'message_search': message_search,
289
304
        'levels': levels,
290
305
        'generate_tags': generate_tags,
291
306
        'delta_type': delta_type,
292
307
        'diff_type': diff_type,
293
308
        'exclude_common_ancestry': exclude_common_ancestry,
 
309
        'signature': signature,
 
310
        'match': match,
 
311
        'omit_merges': omit_merges,
294
312
        # Add 'private' attributes for features that may be deprecated
295
313
        '_match_using_deltas': _match_using_deltas,
296
314
    }
298
316
 
299
317
def _apply_log_request_defaults(rqst):
300
318
    """Apply default values to a request dictionary."""
301
 
    result = _DEFAULT_REQUEST_PARAMS
 
319
    result = _DEFAULT_REQUEST_PARAMS.copy()
302
320
    if rqst:
303
321
        result.update(rqst)
304
322
    return result
305
323
 
306
324
 
 
325
def format_signature_validity(rev_id, repo):
 
326
    """get the signature validity
 
327
 
 
328
    :param rev_id: revision id to validate
 
329
    :param repo: repository of revision
 
330
    :return: human readable string to print to log
 
331
    """
 
332
    from bzrlib import gpg
 
333
 
 
334
    gpg_strategy = gpg.GPGStrategy(None)
 
335
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
 
336
    if result[0] == gpg.SIGNATURE_VALID:
 
337
        return u"valid signature from {0}".format(result[1])
 
338
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
339
        return "unknown key {0}".format(result[1])
 
340
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
341
        return "invalid signature!"
 
342
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
343
        return "no signature"
 
344
 
 
345
 
307
346
class LogGenerator(object):
308
347
    """A generator of log revisions."""
309
348
 
354
393
        # Tweak the LogRequest based on what the LogFormatter can handle.
355
394
        # (There's no point generating stuff if the formatter can't display it.)
356
395
        rqst = self.rqst
357
 
        rqst['levels'] = lf.get_levels()
 
396
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
397
            # user didn't specify levels, use whatever the LF can handle:
 
398
            rqst['levels'] = lf.get_levels()
 
399
 
358
400
        if not getattr(lf, 'supports_tags', False):
359
401
            rqst['generate_tags'] = False
360
402
        if not getattr(lf, 'supports_delta', False):
361
403
            rqst['delta_type'] = None
362
404
        if not getattr(lf, 'supports_diff', False):
363
405
            rqst['diff_type'] = None
 
406
        if not getattr(lf, 'supports_signatures', False):
 
407
            rqst['signature'] = False
364
408
 
365
409
        # Find and print the interesting revisions
366
410
        generator = self._generator_factory(self.branch, rqst)
370
414
 
371
415
    def _generator_factory(self, branch, rqst):
372
416
        """Make the LogGenerator object to use.
373
 
        
 
417
 
374
418
        Subclasses may wish to override this.
375
419
        """
376
420
        return _DefaultLogGenerator(branch, rqst)
400
444
        levels = rqst.get('levels')
401
445
        limit = rqst.get('limit')
402
446
        diff_type = rqst.get('diff_type')
 
447
        show_signature = rqst.get('signature')
 
448
        omit_merges = rqst.get('omit_merges')
403
449
        log_count = 0
404
450
        revision_iterator = self._create_log_revision_iterator()
405
451
        for revs in revision_iterator:
407
453
                # 0 levels means show everything; merge_depth counts from 0
408
454
                if levels != 0 and merge_depth >= levels:
409
455
                    continue
 
456
                if omit_merges and len(rev.parent_ids) > 1:
 
457
                    continue
410
458
                if diff_type is None:
411
459
                    diff = None
412
460
                else:
413
461
                    diff = self._format_diff(rev, rev_id, diff_type)
 
462
                if show_signature:
 
463
                    signature = format_signature_validity(rev_id,
 
464
                                                self.branch.repository)
 
465
                else:
 
466
                    signature = None
414
467
                yield LogRevision(rev, revno, merge_depth, delta,
415
 
                    self.rev_tag_dict.get(rev_id), diff)
 
468
                    self.rev_tag_dict.get(rev_id), diff, signature)
416
469
                if limit:
417
470
                    log_count += 1
418
471
                    if log_count >= limit:
432
485
        else:
433
486
            specific_files = None
434
487
        s = StringIO()
435
 
        path_encoding = osutils.get_diff_header_encoding()
 
488
        path_encoding = get_diff_header_encoding()
436
489
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
437
490
            new_label='', path_encoding=path_encoding)
438
491
        return s.getvalue()
473
526
 
474
527
        # Apply the other filters
475
528
        return make_log_rev_iterator(self.branch, view_revisions,
476
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
529
            rqst.get('delta_type'), rqst.get('match'),
477
530
            file_ids=rqst.get('specific_fileids'),
478
531
            direction=rqst.get('direction'))
479
532
 
492
545
            rqst.get('specific_fileids')[0], view_revisions,
493
546
            include_merges=rqst.get('levels') != 1)
494
547
        return make_log_rev_iterator(self.branch, view_revisions,
495
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
548
            rqst.get('delta_type'), rqst.get('match'))
496
549
 
497
550
 
498
551
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
506
559
             a list of the same tuples.
507
560
    """
508
561
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
509
 
        raise errors.BzrCommandError(
510
 
            '--exclude-common-ancestry requires two different revisions')
 
562
        raise errors.BzrCommandError(gettext(
 
563
            '--exclude-common-ancestry requires two different revisions'))
511
564
    if direction not in ('reverse', 'forward'):
512
 
        raise ValueError('invalid direction %r' % direction)
 
565
        raise ValueError(gettext('invalid direction %r') % direction)
513
566
    br_revno, br_rev_id = branch.last_revision_info()
514
567
    if br_revno == 0:
515
568
        return []
518
571
        and (not generate_merge_revisions
519
572
             or not _has_merges(branch, end_rev_id))):
520
573
        # If a single revision is requested, check we can handle it
521
 
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
522
 
                                           br_revno)
523
 
    elif not generate_merge_revisions:
524
 
        # If we only want to see linear revisions, we can iterate ...
525
 
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
526
 
                                             direction, exclude_common_ancestry)
527
 
        if direction == 'forward':
528
 
            iter_revs = reversed(iter_revs)
529
 
    else:
530
 
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
531
 
                                            direction, delayed_graph_generation,
532
 
                                            exclude_common_ancestry)
533
 
        if direction == 'forward':
534
 
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
574
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
575
                                       br_revno)
 
576
    if not generate_merge_revisions:
 
577
        try:
 
578
            # If we only want to see linear revisions, we can iterate ...
 
579
            iter_revs = _linear_view_revisions(
 
580
                branch, start_rev_id, end_rev_id,
 
581
                exclude_common_ancestry=exclude_common_ancestry)
 
582
            # If a start limit was given and it's not obviously an
 
583
            # ancestor of the end limit, check it before outputting anything
 
584
            if (direction == 'forward'
 
585
                or (start_rev_id and not _is_obvious_ancestor(
 
586
                        branch, start_rev_id, end_rev_id))):
 
587
                    iter_revs = list(iter_revs)
 
588
            if direction == 'forward':
 
589
                iter_revs = reversed(iter_revs)
 
590
            return iter_revs
 
591
        except _StartNotLinearAncestor:
 
592
            # Switch to the slower implementation that may be able to find a
 
593
            # non-obvious ancestor out of the left-hand history.
 
594
            pass
 
595
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
596
                                        direction, delayed_graph_generation,
 
597
                                        exclude_common_ancestry)
 
598
    if direction == 'forward':
 
599
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
535
600
    return iter_revs
536
601
 
537
602
 
540
605
        # It's the tip
541
606
        return [(br_rev_id, br_revno, 0)]
542
607
    else:
543
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
544
 
        revno_str = '.'.join(str(n) for n in revno)
 
608
        revno_str = _compute_revno_str(branch, rev_id)
545
609
        return [(rev_id, revno_str, 0)]
546
610
 
547
611
 
548
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
549
 
                             exclude_common_ancestry=False):
550
 
    result = _linear_view_revisions(
551
 
        branch, start_rev_id, end_rev_id,
552
 
        exclude_common_ancestry=exclude_common_ancestry)
553
 
    # If a start limit was given and it's not obviously an
554
 
    # ancestor of the end limit, check it before outputting anything
555
 
    if direction == 'forward' or (start_rev_id
556
 
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
557
 
        try:
558
 
            result = list(result)
559
 
        except _StartNotLinearAncestor:
560
 
            raise errors.BzrCommandError('Start revision not found in'
561
 
                ' left-hand history of end revision.')
562
 
    return result
563
 
 
564
 
 
565
612
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
566
613
                            delayed_graph_generation,
567
614
                            exclude_common_ancestry=False):
603
650
        except _StartNotLinearAncestor:
604
651
            # A merge was never detected so the lower revision limit can't
605
652
            # be nested down somewhere
606
 
            raise errors.BzrCommandError('Start revision not found in'
607
 
                ' history of end revision.')
 
653
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
654
                ' history of end revision.'))
608
655
 
609
656
    # We exit the loop above because we encounter a revision with merges, from
610
657
    # this revision, we need to switch to _graph_view_revisions.
627
674
    return len(parents) > 1
628
675
 
629
676
 
 
677
def _compute_revno_str(branch, rev_id):
 
678
    """Compute the revno string from a rev_id.
 
679
 
 
680
    :return: The revno string, or None if the revision is not in the supplied
 
681
        branch.
 
682
    """
 
683
    try:
 
684
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
685
    except errors.NoSuchRevision:
 
686
        # The revision must be outside of this branch
 
687
        return None
 
688
    else:
 
689
        return '.'.join(str(n) for n in revno)
 
690
 
 
691
 
630
692
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
631
693
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
632
694
    if start_rev_id and end_rev_id:
633
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
634
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
695
        try:
 
696
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
697
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
698
        except errors.NoSuchRevision:
 
699
            # one or both is not in the branch; not obvious
 
700
            return False
635
701
        if len(start_dotted) == 1 and len(end_dotted) == 1:
636
702
            # both on mainline
637
703
            return start_dotted[0] <= end_dotted[0]
661
727
    """
662
728
    br_revno, br_rev_id = branch.last_revision_info()
663
729
    repo = branch.repository
 
730
    graph = repo.get_graph()
664
731
    if start_rev_id is None and end_rev_id is None:
665
732
        cur_revno = br_revno
666
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
733
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
734
            (_mod_revision.NULL_REVISION,)):
667
735
            yield revision_id, str(cur_revno), 0
668
736
            cur_revno -= 1
669
737
    else:
670
738
        if end_rev_id is None:
671
739
            end_rev_id = br_rev_id
672
740
        found_start = start_rev_id is None
673
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
674
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
675
 
            revno_str = '.'.join(str(n) for n in revno)
 
741
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
742
                (_mod_revision.NULL_REVISION,)):
 
743
            revno_str = _compute_revno_str(branch, revision_id)
676
744
            if not found_start and revision_id == start_rev_id:
677
745
                if not exclude_common_ancestry:
678
746
                    yield revision_id, revno_str, 0
729
797
            yield rev_id, '.'.join(map(str, revno)), merge_depth
730
798
 
731
799
 
732
 
@deprecated_function(deprecated_in((2, 2, 0)))
733
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
734
 
        specific_fileid, generate_merge_revisions):
735
 
    """Calculate the revisions to view.
736
 
 
737
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
738
 
             a list of the same tuples.
739
 
    """
740
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
741
 
        end_revision)
742
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
743
 
        direction, generate_merge_revisions or specific_fileid))
744
 
    if specific_fileid:
745
 
        view_revisions = _filter_revisions_touching_file_id(branch,
746
 
            specific_fileid, view_revisions,
747
 
            include_merges=generate_merge_revisions)
748
 
    return _rebase_merge_depth(view_revisions)
749
 
 
750
 
 
751
800
def _rebase_merge_depth(view_revisions):
752
801
    """Adjust depths upwards so the top level is 0."""
753
802
    # If either the first or last revision have a merge_depth of 0, we're done
797
846
    return log_rev_iterator
798
847
 
799
848
 
800
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
849
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
801
850
    """Create a filtered iterator of log_rev_iterator matching on a regex.
802
851
 
803
852
    :param branch: The branch being logged.
804
853
    :param generate_delta: Whether to generate a delta for each revision.
805
 
    :param search: A user text search string.
 
854
    :param match: A dictionary with properties as keys and lists of strings
 
855
        as values. To match, a revision may match any of the supplied strings
 
856
        within a single property but must match at least one string for each
 
857
        property.
806
858
    :param log_rev_iterator: An input iterator containing all revisions that
807
859
        could be displayed, in lists.
808
860
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
809
861
        delta).
810
862
    """
811
 
    if search is None:
 
863
    if match is None:
812
864
        return log_rev_iterator
813
 
    searchRE = re.compile(search, re.IGNORECASE)
814
 
    return _filter_message_re(searchRE, log_rev_iterator)
815
 
 
816
 
 
817
 
def _filter_message_re(searchRE, log_rev_iterator):
 
865
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
866
                for (k,v) in match.iteritems()]
 
867
    return _filter_re(searchRE, log_rev_iterator)
 
868
 
 
869
 
 
870
def _filter_re(searchRE, log_rev_iterator):
818
871
    for revs in log_rev_iterator:
819
 
        new_revs = []
820
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
821
 
            if searchRE.search(rev.message):
822
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
823
 
        yield new_revs
824
 
 
 
872
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
873
        if new_revs:
 
874
            yield new_revs
 
875
 
 
876
def _match_filter(searchRE, rev):
 
877
    strings = {
 
878
               'message': (rev.message,),
 
879
               'committer': (rev.committer,),
 
880
               'author': (rev.get_apparent_authors()),
 
881
               'bugs': list(rev.iter_bugs())
 
882
               }
 
883
    strings[''] = [item for inner_list in strings.itervalues()
 
884
                   for item in inner_list]
 
885
    for (k,v) in searchRE:
 
886
        if k in strings and not _match_any_filter(strings[k], v):
 
887
            return False
 
888
    return True
 
889
 
 
890
def _match_any_filter(strings, res):
 
891
    return any([filter(None, map(re.search, strings)) for re in res])
825
892
 
826
893
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
827
894
    fileids=None, direction='reverse'):
900
967
 
901
968
def _update_fileids(delta, fileids, stop_on):
902
969
    """Update the set of file-ids to search based on file lifecycle events.
903
 
    
 
970
 
904
971
    :param fileids: a set of fileids to update
905
972
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
906
973
      fileids set once their add or remove entry is detected respectively
947
1014
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
948
1015
        delta).
949
1016
    """
950
 
    repository = branch.repository
951
1017
    num = 9
952
1018
    for batch in log_rev_iterator:
953
1019
        batch = iter(batch)
1002
1068
    if branch_revno != 0:
1003
1069
        if (start_rev_id == _mod_revision.NULL_REVISION
1004
1070
            or end_rev_id == _mod_revision.NULL_REVISION):
1005
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1071
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1006
1072
        if start_revno > end_revno:
1007
 
            raise errors.BzrCommandError("Start revision must be older than "
1008
 
                                         "the end revision.")
 
1073
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1074
                                         "older than the end revision."))
1009
1075
    return (start_rev_id, end_rev_id)
1010
1076
 
1011
1077
 
1060
1126
 
1061
1127
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1062
1128
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1063
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1129
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1064
1130
    if start_revno > end_revno:
1065
 
        raise errors.BzrCommandError("Start revision must be older than "
1066
 
                                     "the end revision.")
 
1131
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1132
                                     "than the end revision."))
1067
1133
 
1068
1134
    if end_revno < start_revno:
1069
1135
        return None, None, None, None
1070
1136
    cur_revno = branch_revno
1071
1137
    rev_nos = {}
1072
1138
    mainline_revs = []
1073
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1074
 
                        branch_last_revision):
 
1139
    graph = branch.repository.get_graph()
 
1140
    for revision_id in graph.iter_lefthand_ancestry(
 
1141
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1075
1142
        if cur_revno < start_revno:
1076
1143
            # We have gone far enough, but we always add 1 more revision
1077
1144
            rev_nos[revision_id] = cur_revno
1091
1158
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1092
1159
 
1093
1160
 
1094
 
@deprecated_function(deprecated_in((2, 2, 0)))
1095
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1096
 
    """Filter view_revisions based on revision ranges.
1097
 
 
1098
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1099
 
            tuples to be filtered.
1100
 
 
1101
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1102
 
            If NONE then all revisions up to the end_rev_id are logged.
1103
 
 
1104
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1105
 
            If NONE then all revisions up to the end of the log are logged.
1106
 
 
1107
 
    :return: The filtered view_revisions.
1108
 
    """
1109
 
    if start_rev_id or end_rev_id:
1110
 
        revision_ids = [r for r, n, d in view_revisions]
1111
 
        if start_rev_id:
1112
 
            start_index = revision_ids.index(start_rev_id)
1113
 
        else:
1114
 
            start_index = 0
1115
 
        if start_rev_id == end_rev_id:
1116
 
            end_index = start_index
1117
 
        else:
1118
 
            if end_rev_id:
1119
 
                end_index = revision_ids.index(end_rev_id)
1120
 
            else:
1121
 
                end_index = len(view_revisions) - 1
1122
 
        # To include the revisions merged into the last revision,
1123
 
        # extend end_rev_id down to, but not including, the next rev
1124
 
        # with the same or lesser merge_depth
1125
 
        end_merge_depth = view_revisions[end_index][2]
1126
 
        try:
1127
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1128
 
                if view_revisions[index][2] <= end_merge_depth:
1129
 
                    end_index = index - 1
1130
 
                    break
1131
 
        except IndexError:
1132
 
            # if the search falls off the end then log to the end as well
1133
 
            end_index = len(view_revisions) - 1
1134
 
        view_revisions = view_revisions[start_index:end_index+1]
1135
 
    return view_revisions
1136
 
 
1137
 
 
1138
1161
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1139
1162
    include_merges=True):
1140
1163
    r"""Return the list of revision ids which touch a given file id.
1143
1166
    This includes the revisions which directly change the file id,
1144
1167
    and the revisions which merge these changes. So if the
1145
1168
    revision graph is::
 
1169
 
1146
1170
        A-.
1147
1171
        |\ \
1148
1172
        B C E
1175
1199
    """
1176
1200
    # Lookup all possible text keys to determine which ones actually modified
1177
1201
    # the file.
 
1202
    graph = branch.repository.get_file_graph()
 
1203
    get_parent_map = graph.get_parent_map
1178
1204
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1179
1205
    next_keys = None
1180
1206
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1184
1210
    #       indexing layer. We might consider passing in hints as to the known
1185
1211
    #       access pattern (sparse/clustered, high success rate/low success
1186
1212
    #       rate). This particular access is clustered with a low success rate.
1187
 
    get_parent_map = branch.repository.texts.get_parent_map
1188
1213
    modified_text_revisions = set()
1189
1214
    chunk_size = 1000
1190
1215
    for start in xrange(0, len(text_keys), chunk_size):
1217
1242
    return result
1218
1243
 
1219
1244
 
1220
 
@deprecated_function(deprecated_in((2, 2, 0)))
1221
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1222
 
                       include_merges=True):
1223
 
    """Produce an iterator of revisions to show
1224
 
    :return: an iterator of (revision_id, revno, merge_depth)
1225
 
    (if there is no revno for a revision, None is supplied)
1226
 
    """
1227
 
    if not include_merges:
1228
 
        revision_ids = mainline_revs[1:]
1229
 
        if direction == 'reverse':
1230
 
            revision_ids.reverse()
1231
 
        for revision_id in revision_ids:
1232
 
            yield revision_id, str(rev_nos[revision_id]), 0
1233
 
        return
1234
 
    graph = branch.repository.get_graph()
1235
 
    # This asks for all mainline revisions, which means we only have to spider
1236
 
    # sideways, rather than depth history. That said, its still size-of-history
1237
 
    # and should be addressed.
1238
 
    # mainline_revisions always includes an extra revision at the beginning, so
1239
 
    # don't request it.
1240
 
    parent_map = dict(((key, value) for key, value in
1241
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1242
 
    # filter out ghosts; merge_sort errors on ghosts.
1243
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1244
 
    merge_sorted_revisions = tsort.merge_sort(
1245
 
        rev_graph,
1246
 
        mainline_revs[-1],
1247
 
        mainline_revs,
1248
 
        generate_revno=True)
1249
 
 
1250
 
    if direction == 'forward':
1251
 
        # forward means oldest first.
1252
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1253
 
    elif direction != 'reverse':
1254
 
        raise ValueError('invalid direction %r' % direction)
1255
 
 
1256
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1257
 
         ) in merge_sorted_revisions:
1258
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1259
 
 
1260
 
 
1261
1245
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1262
1246
    """Reverse revisions by depth.
1263
1247
 
1298
1282
    """
1299
1283
 
1300
1284
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1301
 
                 tags=None, diff=None):
 
1285
                 tags=None, diff=None, signature=None):
1302
1286
        self.rev = rev
1303
 
        self.revno = str(revno)
 
1287
        if revno is None:
 
1288
            self.revno = None
 
1289
        else:
 
1290
            self.revno = str(revno)
1304
1291
        self.merge_depth = merge_depth
1305
1292
        self.delta = delta
1306
1293
        self.tags = tags
1307
1294
        self.diff = diff
 
1295
        self.signature = signature
1308
1296
 
1309
1297
 
1310
1298
class LogFormatter(object):
1319
1307
    to indicate which LogRevision attributes it supports:
1320
1308
 
1321
1309
    - supports_delta must be True if this log formatter supports delta.
1322
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1323
 
        attribute describes whether the 'short_status' format (1) or the long
1324
 
        one (2) should be used.
 
1310
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1311
      attribute describes whether the 'short_status' format (1) or the long
 
1312
      one (2) should be used.
1325
1313
 
1326
1314
    - supports_merge_revisions must be True if this log formatter supports
1327
 
        merge revisions.  If not, then only mainline revisions will be passed
1328
 
        to the formatter.
 
1315
      merge revisions.  If not, then only mainline revisions will be passed
 
1316
      to the formatter.
1329
1317
 
1330
1318
    - preferred_levels is the number of levels this formatter defaults to.
1331
 
        The default value is zero meaning display all levels.
1332
 
        This value is only relevant if supports_merge_revisions is True.
 
1319
      The default value is zero meaning display all levels.
 
1320
      This value is only relevant if supports_merge_revisions is True.
1333
1321
 
1334
1322
    - supports_tags must be True if this log formatter supports tags.
1335
 
        Otherwise the tags attribute may not be populated.
 
1323
      Otherwise the tags attribute may not be populated.
1336
1324
 
1337
1325
    - supports_diff must be True if this log formatter supports diffs.
1338
 
        Otherwise the diff attribute may not be populated.
 
1326
      Otherwise the diff attribute may not be populated.
 
1327
 
 
1328
    - supports_signatures must be True if this log formatter supports GPG
 
1329
      signatures.
1339
1330
 
1340
1331
    Plugins can register functions to show custom revision properties using
1341
1332
    the properties_handler_registry. The registered function
1342
 
    must respect the following interface description:
 
1333
    must respect the following interface description::
 
1334
 
1343
1335
        def my_show_properties(properties_dict):
1344
1336
            # code that returns a dict {'name':'value'} of the properties
1345
1337
            # to be shown
1352
1344
        """Create a LogFormatter.
1353
1345
 
1354
1346
        :param to_file: the file to output to
1355
 
        :param to_exact_file: if set, gives an output stream to which 
 
1347
        :param to_exact_file: if set, gives an output stream to which
1356
1348
             non-Unicode diffs are written.
1357
1349
        :param show_ids: if True, revision-ids are to be displayed
1358
1350
        :param show_timezone: the timezone to use
1409
1401
            if advice_sep:
1410
1402
                self.to_file.write(advice_sep)
1411
1403
            self.to_file.write(
1412
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1404
                "Use --include-merged or -n0 to see merged revisions.\n")
1413
1405
 
1414
1406
    def get_advice_separator(self):
1415
1407
        """Get the text separating the log from the closing advice."""
1532
1524
    supports_delta = True
1533
1525
    supports_tags = True
1534
1526
    supports_diff = True
 
1527
    supports_signatures = True
1535
1528
 
1536
1529
    def __init__(self, *args, **kwargs):
1537
1530
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1557
1550
                self.merge_marker(revision)))
1558
1551
        if revision.tags:
1559
1552
            lines.append('tags: %s' % (', '.join(revision.tags)))
1560
 
        if self.show_ids:
 
1553
        if self.show_ids or revision.revno is None:
1561
1554
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1555
        if self.show_ids:
1562
1556
            for parent_id in revision.rev.parent_ids:
1563
1557
                lines.append('parent: %s' % (parent_id,))
1564
1558
        lines.extend(self.custom_properties(revision.rev))
1575
1569
 
1576
1570
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1577
1571
 
 
1572
        if revision.signature is not None:
 
1573
            lines.append('signature: ' + revision.signature)
 
1574
 
1578
1575
        lines.append('message:')
1579
1576
        if not revision.rev.message:
1580
1577
            lines.append('  (no message)')
1589
1586
        if revision.delta is not None:
1590
1587
            # Use the standard status output to display changes
1591
1588
            from bzrlib.delta import report_delta
1592
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1589
            report_delta(to_file, revision.delta, short_status=False,
1593
1590
                         show_ids=self.show_ids, indent=indent)
1594
1591
        if revision.diff is not None:
1595
1592
            to_file.write(indent + 'diff:\n')
1627
1624
        indent = '    ' * depth
1628
1625
        revno_width = self.revno_width_by_depth.get(depth)
1629
1626
        if revno_width is None:
1630
 
            if revision.revno.find('.') == -1:
 
1627
            if revision.revno is None or revision.revno.find('.') == -1:
1631
1628
                # mainline revno, e.g. 12345
1632
1629
                revno_width = 5
1633
1630
            else:
1641
1638
        if revision.tags:
1642
1639
            tags = ' {%s}' % (', '.join(revision.tags))
1643
1640
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1644
 
                revision.revno, self.short_author(revision.rev),
 
1641
                revision.revno or "", self.short_author(revision.rev),
1645
1642
                format_date(revision.rev.timestamp,
1646
1643
                            revision.rev.timezone or 0,
1647
1644
                            self.show_timezone, date_fmt="%Y-%m-%d",
1648
1645
                            show_offset=False),
1649
1646
                tags, self.merge_marker(revision)))
1650
1647
        self.show_properties(revision.rev, indent+offset)
1651
 
        if self.show_ids:
 
1648
        if self.show_ids or revision.revno is None:
1652
1649
            to_file.write(indent + offset + 'revision-id:%s\n'
1653
1650
                          % (revision.rev.revision_id,))
1654
1651
        if not revision.rev.message:
1661
1658
        if revision.delta is not None:
1662
1659
            # Use the standard status output to display changes
1663
1660
            from bzrlib.delta import report_delta
1664
 
            report_delta(to_file, revision.delta, 
1665
 
                         short_status=self.delta_format==1, 
 
1661
            report_delta(to_file, revision.delta,
 
1662
                         short_status=self.delta_format==1,
1666
1663
                         show_ids=self.show_ids, indent=indent + offset)
1667
1664
        if revision.diff is not None:
1668
1665
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1707
1704
 
1708
1705
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1709
1706
        """Format log info into one string. Truncate tail of string
1710
 
        :param  revno:      revision number or None.
1711
 
                            Revision numbers counts from 1.
1712
 
        :param  rev:        revision object
1713
 
        :param  max_chars:  maximum length of resulting string
1714
 
        :param  tags:       list of tags or None
1715
 
        :param  prefix:     string to prefix each line
1716
 
        :return:            formatted truncated string
 
1707
 
 
1708
        :param revno:      revision number or None.
 
1709
                           Revision numbers counts from 1.
 
1710
        :param rev:        revision object
 
1711
        :param max_chars:  maximum length of resulting string
 
1712
        :param tags:       list of tags or None
 
1713
        :param prefix:     string to prefix each line
 
1714
        :return:           formatted truncated string
1717
1715
        """
1718
1716
        out = []
1719
1717
        if revno:
1720
1718
            # show revno only when is not None
1721
1719
            out.append("%s:" % revno)
1722
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1720
        if max_chars is not None:
 
1721
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1722
        else:
 
1723
            out.append(self.short_author(rev))
1723
1724
        out.append(self.date_string(rev))
1724
1725
        if len(rev.parent_ids) > 1:
1725
1726
            out.append('[merge]')
1784
1785
        return self.get(name)(*args, **kwargs)
1785
1786
 
1786
1787
    def get_default(self, branch):
1787
 
        return self.get(branch.get_config().log_format())
 
1788
        c = branch.get_config_stack()
 
1789
        return self.get(c.get('log_format'))
1788
1790
 
1789
1791
 
1790
1792
log_formatter_registry = LogFormatterRegistry()
1791
1793
 
1792
1794
 
1793
1795
log_formatter_registry.register('short', ShortLogFormatter,
1794
 
                                'Moderately short log format')
 
1796
                                'Moderately short log format.')
1795
1797
log_formatter_registry.register('long', LongLogFormatter,
1796
 
                                'Detailed log format')
 
1798
                                'Detailed log format.')
1797
1799
log_formatter_registry.register('line', LineLogFormatter,
1798
 
                                'Log format with one line per revision')
 
1800
                                'Log format with one line per revision.')
1799
1801
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1800
 
                                'Format used by GNU ChangeLog files')
 
1802
                                'Format used by GNU ChangeLog files.')
1801
1803
 
1802
1804
 
1803
1805
def register_formatter(name, formatter):
1813
1815
    try:
1814
1816
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1815
1817
    except KeyError:
1816
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1818
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1817
1819
 
1818
1820
 
1819
1821
def author_list_all(rev):
1844
1846
                              'The committer')
1845
1847
 
1846
1848
 
1847
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1848
 
    # deprecated; for compatibility
1849
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1850
 
    lf.show(revno, rev, delta)
1851
 
 
1852
 
 
1853
1849
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1854
1850
                           log_format='long'):
1855
1851
    """Show the change in revision history comparing the old revision history to the new one.
1918
1914
    old_revisions = set()
1919
1915
    new_history = []
1920
1916
    new_revisions = set()
1921
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1922
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1917
    graph = repository.get_graph()
 
1918
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1919
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1923
1920
    stop_revision = None
1924
1921
    do_old = True
1925
1922
    do_new = True
2017
2014
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2018
2015
      branch will be read-locked.
2019
2016
    """
2020
 
    from builtins import _get_revision_range
2021
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
2017
    from bzrlib.builtins import _get_revision_range
 
2018
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2019
        file_list[0])
2022
2020
    add_cleanup(b.lock_read().unlock)
2023
2021
    # XXX: It's damn messy converting a list of paths to relative paths when
2024
2022
    # those paths might be deleted ones, they might be on a case-insensitive
2113
2111
                          len(row) > 1 and row[1] == 'fixed']
2114
2112
 
2115
2113
        if fixed_bug_urls:
2116
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2114
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2115
                    ' '.join(fixed_bug_urls)}
2117
2116
    return {}
2118
2117
 
2119
2118
properties_handler_registry.register('bugs_properties_handler',