~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Patch Queue Manager
  • Date: 2013-07-14 10:59:28 UTC
  • mfrom: (6579.2.1 1195783-platform-utf8)
  • Revision ID: pqm@pqm.ubuntu.com-20130714105928-78j748r1djstxmo1
(vila) Make 'bzr version' support utf8 platform names. (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
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,
75
75
    revisionspec,
76
76
    tsort,
77
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
78
79
""")
79
80
 
80
81
from bzrlib import (
 
82
    lazy_regex,
81
83
    registry,
82
84
    )
83
85
from bzrlib.osutils import (
87
89
    get_terminal_encoding,
88
90
    terminal_width,
89
91
    )
90
 
from bzrlib.symbol_versioning import (
91
 
    deprecated_function,
92
 
    deprecated_in,
93
 
    )
94
92
 
95
93
 
96
94
def find_touching_revisions(branch, file_id):
107
105
    last_ie = None
108
106
    last_path = None
109
107
    revno = 1
110
 
    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):
111
112
        this_inv = branch.repository.get_inventory(revision_id)
112
 
        if file_id in this_inv:
 
113
        if this_inv.has_id(file_id):
113
114
            this_ie = this_inv[file_id]
114
115
            this_path = this_inv.id2path(file_id)
115
116
        else:
137
138
        revno += 1
138
139
 
139
140
 
140
 
def _enumerate_history(branch):
141
 
    rh = []
142
 
    revno = 1
143
 
    for rev_id in branch.revision_history():
144
 
        rh.append((revno, rev_id))
145
 
        revno += 1
146
 
    return rh
147
 
 
148
 
 
149
141
def show_log(branch,
150
142
             lf,
151
143
             specific_fileid=None,
155
147
             end_revision=None,
156
148
             search=None,
157
149
             limit=None,
158
 
             show_diff=False):
 
150
             show_diff=False,
 
151
             match=None):
159
152
    """Write out human-readable log of commits to this branch.
160
153
 
161
154
    This function is being retained for backwards compatibility but
184
177
        if None or 0.
185
178
 
186
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.
187
183
    """
188
184
    # Convert old-style parameters to new-style parameters
189
185
    if specific_fileid is not None:
213
209
    Logger(branch, rqst).show(lf)
214
210
 
215
211
 
216
 
# 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
217
213
# make_log_request_dict() below
218
214
_DEFAULT_REQUEST_PARAMS = {
219
215
    'direction': 'reverse',
220
 
    'levels': 1,
 
216
    'levels': None,
221
217
    'generate_tags': True,
222
218
    'exclude_common_ancestry': False,
223
219
    '_match_using_deltas': True,
226
222
 
227
223
def make_log_request_dict(direction='reverse', specific_fileids=None,
228
224
                          start_revision=None, end_revision=None, limit=None,
229
 
                          message_search=None, levels=1, generate_tags=True,
 
225
                          message_search=None, levels=None, generate_tags=True,
230
226
                          delta_type=None,
231
227
                          diff_type=None, _match_using_deltas=True,
232
 
                          exclude_common_ancestry=False,
 
228
                          exclude_common_ancestry=False, match=None,
 
229
                          signature=False, omit_merges=False,
233
230
                          ):
234
231
    """Convenience function for making a logging request dictionary.
235
232
 
256
253
      matching commit messages
257
254
 
258
255
    :param levels: the number of levels of revisions to
259
 
      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.
260
258
 
261
259
    :param generate_tags: If True, include tags for matched revisions.
262
 
 
 
260
`
263
261
    :param delta_type: Either 'full', 'partial' or None.
264
262
      'full' means generate the complete delta - adds/deletes/modifies/etc;
265
263
      'partial' means filter the delta using specific_fileids;
277
275
 
278
276
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
279
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
 
280
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] }
281
298
    return {
282
299
        'direction': direction,
283
300
        'specific_fileids': specific_fileids,
284
301
        'start_revision': start_revision,
285
302
        'end_revision': end_revision,
286
303
        'limit': limit,
287
 
        'message_search': message_search,
288
304
        'levels': levels,
289
305
        'generate_tags': generate_tags,
290
306
        'delta_type': delta_type,
291
307
        'diff_type': diff_type,
292
308
        'exclude_common_ancestry': exclude_common_ancestry,
 
309
        'signature': signature,
 
310
        'match': match,
 
311
        'omit_merges': omit_merges,
293
312
        # Add 'private' attributes for features that may be deprecated
294
313
        '_match_using_deltas': _match_using_deltas,
295
314
    }
303
322
    return result
304
323
 
305
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 "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
 
306
346
class LogGenerator(object):
307
347
    """A generator of log revisions."""
308
348
 
353
393
        # Tweak the LogRequest based on what the LogFormatter can handle.
354
394
        # (There's no point generating stuff if the formatter can't display it.)
355
395
        rqst = self.rqst
356
 
        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
 
357
400
        if not getattr(lf, 'supports_tags', False):
358
401
            rqst['generate_tags'] = False
359
402
        if not getattr(lf, 'supports_delta', False):
360
403
            rqst['delta_type'] = None
361
404
        if not getattr(lf, 'supports_diff', False):
362
405
            rqst['diff_type'] = None
 
406
        if not getattr(lf, 'supports_signatures', False):
 
407
            rqst['signature'] = False
363
408
 
364
409
        # Find and print the interesting revisions
365
410
        generator = self._generator_factory(self.branch, rqst)
369
414
 
370
415
    def _generator_factory(self, branch, rqst):
371
416
        """Make the LogGenerator object to use.
372
 
        
 
417
 
373
418
        Subclasses may wish to override this.
374
419
        """
375
420
        return _DefaultLogGenerator(branch, rqst)
399
444
        levels = rqst.get('levels')
400
445
        limit = rqst.get('limit')
401
446
        diff_type = rqst.get('diff_type')
 
447
        show_signature = rqst.get('signature')
 
448
        omit_merges = rqst.get('omit_merges')
402
449
        log_count = 0
403
450
        revision_iterator = self._create_log_revision_iterator()
404
451
        for revs in revision_iterator:
406
453
                # 0 levels means show everything; merge_depth counts from 0
407
454
                if levels != 0 and merge_depth >= levels:
408
455
                    continue
 
456
                if omit_merges and len(rev.parent_ids) > 1:
 
457
                    continue
409
458
                if diff_type is None:
410
459
                    diff = None
411
460
                else:
412
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
413
467
                yield LogRevision(rev, revno, merge_depth, delta,
414
 
                    self.rev_tag_dict.get(rev_id), diff)
 
468
                    self.rev_tag_dict.get(rev_id), diff, signature)
415
469
                if limit:
416
470
                    log_count += 1
417
471
                    if log_count >= limit:
472
526
 
473
527
        # Apply the other filters
474
528
        return make_log_rev_iterator(self.branch, view_revisions,
475
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
529
            rqst.get('delta_type'), rqst.get('match'),
476
530
            file_ids=rqst.get('specific_fileids'),
477
531
            direction=rqst.get('direction'))
478
532
 
491
545
            rqst.get('specific_fileids')[0], view_revisions,
492
546
            include_merges=rqst.get('levels') != 1)
493
547
        return make_log_rev_iterator(self.branch, view_revisions,
494
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
548
            rqst.get('delta_type'), rqst.get('match'))
495
549
 
496
550
 
497
551
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
559
             a list of the same tuples.
506
560
    """
507
561
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
 
        raise errors.BzrCommandError(
509
 
            '--exclude-common-ancestry requires two different revisions')
 
562
        raise errors.BzrCommandError(gettext(
 
563
            '--exclude-common-ancestry requires two different revisions'))
510
564
    if direction not in ('reverse', 'forward'):
511
 
        raise ValueError('invalid direction %r' % direction)
 
565
        raise ValueError(gettext('invalid direction %r') % direction)
512
566
    br_revno, br_rev_id = branch.last_revision_info()
513
567
    if br_revno == 0:
514
568
        return []
517
571
        and (not generate_merge_revisions
518
572
             or not _has_merges(branch, end_rev_id))):
519
573
        # 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, exclude_common_ancestry)
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)))
 
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)))
534
600
    return iter_revs
535
601
 
536
602
 
543
609
        return [(rev_id, revno_str, 0)]
544
610
 
545
611
 
546
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
547
 
                             exclude_common_ancestry=False):
548
 
    result = _linear_view_revisions(
549
 
        branch, start_rev_id, end_rev_id,
550
 
        exclude_common_ancestry=exclude_common_ancestry)
551
 
    # If a start limit was given and it's not obviously an
552
 
    # ancestor of the end limit, check it before outputting anything
553
 
    if direction == 'forward' or (start_rev_id
554
 
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
555
 
        try:
556
 
            result = list(result)
557
 
        except _StartNotLinearAncestor:
558
 
            raise errors.BzrCommandError('Start revision not found in'
559
 
                ' left-hand history of end revision.')
560
 
    return result
561
 
 
562
 
 
563
612
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
564
613
                            delayed_graph_generation,
565
614
                            exclude_common_ancestry=False):
601
650
        except _StartNotLinearAncestor:
602
651
            # A merge was never detected so the lower revision limit can't
603
652
            # be nested down somewhere
604
 
            raise errors.BzrCommandError('Start revision not found in'
605
 
                ' history of end revision.')
 
653
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
654
                ' history of end revision.'))
606
655
 
607
656
    # We exit the loop above because we encounter a revision with merges, from
608
657
    # this revision, we need to switch to _graph_view_revisions.
678
727
    """
679
728
    br_revno, br_rev_id = branch.last_revision_info()
680
729
    repo = branch.repository
 
730
    graph = repo.get_graph()
681
731
    if start_rev_id is None and end_rev_id is None:
682
732
        cur_revno = br_revno
683
 
        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,)):
684
735
            yield revision_id, str(cur_revno), 0
685
736
            cur_revno -= 1
686
737
    else:
687
738
        if end_rev_id is None:
688
739
            end_rev_id = br_rev_id
689
740
        found_start = start_rev_id is None
690
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
 
741
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
742
                (_mod_revision.NULL_REVISION,)):
691
743
            revno_str = _compute_revno_str(branch, revision_id)
692
744
            if not found_start and revision_id == start_rev_id:
693
745
                if not exclude_common_ancestry:
745
797
            yield rev_id, '.'.join(map(str, revno)), merge_depth
746
798
 
747
799
 
748
 
@deprecated_function(deprecated_in((2, 2, 0)))
749
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
750
 
        specific_fileid, generate_merge_revisions):
751
 
    """Calculate the revisions to view.
752
 
 
753
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
754
 
             a list of the same tuples.
755
 
    """
756
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
757
 
        end_revision)
758
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
759
 
        direction, generate_merge_revisions or specific_fileid))
760
 
    if specific_fileid:
761
 
        view_revisions = _filter_revisions_touching_file_id(branch,
762
 
            specific_fileid, view_revisions,
763
 
            include_merges=generate_merge_revisions)
764
 
    return _rebase_merge_depth(view_revisions)
765
 
 
766
 
 
767
800
def _rebase_merge_depth(view_revisions):
768
801
    """Adjust depths upwards so the top level is 0."""
769
802
    # If either the first or last revision have a merge_depth of 0, we're done
813
846
    return log_rev_iterator
814
847
 
815
848
 
816
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
849
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
817
850
    """Create a filtered iterator of log_rev_iterator matching on a regex.
818
851
 
819
852
    :param branch: The branch being logged.
820
853
    :param generate_delta: Whether to generate a delta for each revision.
821
 
    :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.
822
858
    :param log_rev_iterator: An input iterator containing all revisions that
823
859
        could be displayed, in lists.
824
860
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
825
861
        delta).
826
862
    """
827
 
    if search is None:
 
863
    if match is None:
828
864
        return log_rev_iterator
829
 
    searchRE = re.compile(search, re.IGNORECASE)
830
 
    return _filter_message_re(searchRE, log_rev_iterator)
831
 
 
832
 
 
833
 
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):
834
871
    for revs in log_rev_iterator:
835
 
        new_revs = []
836
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
837
 
            if searchRE.search(rev.message):
838
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
839
 
        yield new_revs
840
 
 
 
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])
841
892
 
842
893
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
843
894
    fileids=None, direction='reverse'):
916
967
 
917
968
def _update_fileids(delta, fileids, stop_on):
918
969
    """Update the set of file-ids to search based on file lifecycle events.
919
 
    
 
970
 
920
971
    :param fileids: a set of fileids to update
921
972
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
922
973
      fileids set once their add or remove entry is detected respectively
963
1014
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
964
1015
        delta).
965
1016
    """
966
 
    repository = branch.repository
967
1017
    num = 9
968
1018
    for batch in log_rev_iterator:
969
1019
        batch = iter(batch)
1018
1068
    if branch_revno != 0:
1019
1069
        if (start_rev_id == _mod_revision.NULL_REVISION
1020
1070
            or end_rev_id == _mod_revision.NULL_REVISION):
1021
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1071
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1022
1072
        if start_revno > end_revno:
1023
 
            raise errors.BzrCommandError("Start revision must be older than "
1024
 
                                         "the end revision.")
 
1073
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1074
                                         "older than the end revision."))
1025
1075
    return (start_rev_id, end_rev_id)
1026
1076
 
1027
1077
 
1076
1126
 
1077
1127
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1078
1128
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1079
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1129
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1080
1130
    if start_revno > end_revno:
1081
 
        raise errors.BzrCommandError("Start revision must be older than "
1082
 
                                     "the end revision.")
 
1131
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1132
                                     "than the end revision."))
1083
1133
 
1084
1134
    if end_revno < start_revno:
1085
1135
        return None, None, None, None
1086
1136
    cur_revno = branch_revno
1087
1137
    rev_nos = {}
1088
1138
    mainline_revs = []
1089
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1090
 
                        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,)):
1091
1142
        if cur_revno < start_revno:
1092
1143
            # We have gone far enough, but we always add 1 more revision
1093
1144
            rev_nos[revision_id] = cur_revno
1107
1158
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1108
1159
 
1109
1160
 
1110
 
@deprecated_function(deprecated_in((2, 2, 0)))
1111
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1112
 
    """Filter view_revisions based on revision ranges.
1113
 
 
1114
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1115
 
            tuples to be filtered.
1116
 
 
1117
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1118
 
            If NONE then all revisions up to the end_rev_id are logged.
1119
 
 
1120
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1121
 
            If NONE then all revisions up to the end of the log are logged.
1122
 
 
1123
 
    :return: The filtered view_revisions.
1124
 
    """
1125
 
    if start_rev_id or end_rev_id:
1126
 
        revision_ids = [r for r, n, d in view_revisions]
1127
 
        if start_rev_id:
1128
 
            start_index = revision_ids.index(start_rev_id)
1129
 
        else:
1130
 
            start_index = 0
1131
 
        if start_rev_id == end_rev_id:
1132
 
            end_index = start_index
1133
 
        else:
1134
 
            if end_rev_id:
1135
 
                end_index = revision_ids.index(end_rev_id)
1136
 
            else:
1137
 
                end_index = len(view_revisions) - 1
1138
 
        # To include the revisions merged into the last revision,
1139
 
        # extend end_rev_id down to, but not including, the next rev
1140
 
        # with the same or lesser merge_depth
1141
 
        end_merge_depth = view_revisions[end_index][2]
1142
 
        try:
1143
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1144
 
                if view_revisions[index][2] <= end_merge_depth:
1145
 
                    end_index = index - 1
1146
 
                    break
1147
 
        except IndexError:
1148
 
            # if the search falls off the end then log to the end as well
1149
 
            end_index = len(view_revisions) - 1
1150
 
        view_revisions = view_revisions[start_index:end_index+1]
1151
 
    return view_revisions
1152
 
 
1153
 
 
1154
1161
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1155
1162
    include_merges=True):
1156
1163
    r"""Return the list of revision ids which touch a given file id.
1235
1242
    return result
1236
1243
 
1237
1244
 
1238
 
@deprecated_function(deprecated_in((2, 2, 0)))
1239
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1240
 
                       include_merges=True):
1241
 
    """Produce an iterator of revisions to show
1242
 
    :return: an iterator of (revision_id, revno, merge_depth)
1243
 
    (if there is no revno for a revision, None is supplied)
1244
 
    """
1245
 
    if not include_merges:
1246
 
        revision_ids = mainline_revs[1:]
1247
 
        if direction == 'reverse':
1248
 
            revision_ids.reverse()
1249
 
        for revision_id in revision_ids:
1250
 
            yield revision_id, str(rev_nos[revision_id]), 0
1251
 
        return
1252
 
    graph = branch.repository.get_graph()
1253
 
    # This asks for all mainline revisions, which means we only have to spider
1254
 
    # sideways, rather than depth history. That said, its still size-of-history
1255
 
    # and should be addressed.
1256
 
    # mainline_revisions always includes an extra revision at the beginning, so
1257
 
    # don't request it.
1258
 
    parent_map = dict(((key, value) for key, value in
1259
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1260
 
    # filter out ghosts; merge_sort errors on ghosts.
1261
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1262
 
    merge_sorted_revisions = tsort.merge_sort(
1263
 
        rev_graph,
1264
 
        mainline_revs[-1],
1265
 
        mainline_revs,
1266
 
        generate_revno=True)
1267
 
 
1268
 
    if direction == 'forward':
1269
 
        # forward means oldest first.
1270
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1271
 
    elif direction != 'reverse':
1272
 
        raise ValueError('invalid direction %r' % direction)
1273
 
 
1274
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1275
 
         ) in merge_sorted_revisions:
1276
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1277
 
 
1278
 
 
1279
1245
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1280
1246
    """Reverse revisions by depth.
1281
1247
 
1316
1282
    """
1317
1283
 
1318
1284
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1319
 
                 tags=None, diff=None):
 
1285
                 tags=None, diff=None, signature=None):
1320
1286
        self.rev = rev
1321
1287
        if revno is None:
1322
1288
            self.revno = None
1326
1292
        self.delta = delta
1327
1293
        self.tags = tags
1328
1294
        self.diff = diff
 
1295
        self.signature = signature
1329
1296
 
1330
1297
 
1331
1298
class LogFormatter(object):
1358
1325
    - supports_diff must be True if this log formatter supports diffs.
1359
1326
      Otherwise the diff attribute may not be populated.
1360
1327
 
 
1328
    - supports_signatures must be True if this log formatter supports GPG
 
1329
      signatures.
 
1330
 
1361
1331
    Plugins can register functions to show custom revision properties using
1362
1332
    the properties_handler_registry. The registered function
1363
1333
    must respect the following interface description::
1374
1344
        """Create a LogFormatter.
1375
1345
 
1376
1346
        :param to_file: the file to output to
1377
 
        :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
1378
1348
             non-Unicode diffs are written.
1379
1349
        :param show_ids: if True, revision-ids are to be displayed
1380
1350
        :param show_timezone: the timezone to use
1431
1401
            if advice_sep:
1432
1402
                self.to_file.write(advice_sep)
1433
1403
            self.to_file.write(
1434
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1404
                "Use --include-merged or -n0 to see merged revisions.\n")
1435
1405
 
1436
1406
    def get_advice_separator(self):
1437
1407
        """Get the text separating the log from the closing advice."""
1554
1524
    supports_delta = True
1555
1525
    supports_tags = True
1556
1526
    supports_diff = True
 
1527
    supports_signatures = True
1557
1528
 
1558
1529
    def __init__(self, *args, **kwargs):
1559
1530
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1598
1569
 
1599
1570
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1600
1571
 
 
1572
        if revision.signature is not None:
 
1573
            lines.append('signature: ' + revision.signature)
 
1574
 
1601
1575
        lines.append('message:')
1602
1576
        if not revision.rev.message:
1603
1577
            lines.append('  (no message)')
1612
1586
        if revision.delta is not None:
1613
1587
            # Use the standard status output to display changes
1614
1588
            from bzrlib.delta import report_delta
1615
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1589
            report_delta(to_file, revision.delta, short_status=False,
1616
1590
                         show_ids=self.show_ids, indent=indent)
1617
1591
        if revision.diff is not None:
1618
1592
            to_file.write(indent + 'diff:\n')
1684
1658
        if revision.delta is not None:
1685
1659
            # Use the standard status output to display changes
1686
1660
            from bzrlib.delta import report_delta
1687
 
            report_delta(to_file, revision.delta, 
1688
 
                         short_status=self.delta_format==1, 
 
1661
            report_delta(to_file, revision.delta,
 
1662
                         short_status=self.delta_format==1,
1689
1663
                         show_ids=self.show_ids, indent=indent + offset)
1690
1664
        if revision.diff is not None:
1691
1665
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1811
1785
        return self.get(name)(*args, **kwargs)
1812
1786
 
1813
1787
    def get_default(self, branch):
1814
 
        return self.get(branch.get_config().log_format())
 
1788
        c = branch.get_config_stack()
 
1789
        return self.get(c.get('log_format'))
1815
1790
 
1816
1791
 
1817
1792
log_formatter_registry = LogFormatterRegistry()
1818
1793
 
1819
1794
 
1820
1795
log_formatter_registry.register('short', ShortLogFormatter,
1821
 
                                'Moderately short log format')
 
1796
                                'Moderately short log format.')
1822
1797
log_formatter_registry.register('long', LongLogFormatter,
1823
 
                                'Detailed log format')
 
1798
                                'Detailed log format.')
1824
1799
log_formatter_registry.register('line', LineLogFormatter,
1825
 
                                'Log format with one line per revision')
 
1800
                                'Log format with one line per revision.')
1826
1801
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1827
 
                                'Format used by GNU ChangeLog files')
 
1802
                                'Format used by GNU ChangeLog files.')
1828
1803
 
1829
1804
 
1830
1805
def register_formatter(name, formatter):
1840
1815
    try:
1841
1816
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1842
1817
    except KeyError:
1843
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1818
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1844
1819
 
1845
1820
 
1846
1821
def author_list_all(rev):
1871
1846
                              'The committer')
1872
1847
 
1873
1848
 
1874
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1875
 
    # deprecated; for compatibility
1876
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1877
 
    lf.show(revno, rev, delta)
1878
 
 
1879
 
 
1880
1849
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1881
1850
                           log_format='long'):
1882
1851
    """Show the change in revision history comparing the old revision history to the new one.
1945
1914
    old_revisions = set()
1946
1915
    new_history = []
1947
1916
    new_revisions = set()
1948
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1949
 
    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)
1950
1920
    stop_revision = None
1951
1921
    do_old = True
1952
1922
    do_new = True
2044
2014
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2045
2015
      branch will be read-locked.
2046
2016
    """
2047
 
    from builtins import _get_revision_range
2048
 
    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])
2049
2020
    add_cleanup(b.lock_read().unlock)
2050
2021
    # XXX: It's damn messy converting a list of paths to relative paths when
2051
2022
    # those paths might be deleted ones, they might be on a case-insensitive
2140
2111
                          len(row) > 1 and row[1] == 'fixed']
2141
2112
 
2142
2113
        if fixed_bug_urls:
2143
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2114
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2115
                    ' '.join(fixed_bug_urls)}
2144
2116
    return {}
2145
2117
 
2146
2118
properties_handler_registry.register('bugs_properties_handler',