~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

Abbreviate pack_stat struct format to '>6L'

Show diffs side-by-side

added added

removed removed

Lines of Context:
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):
109
107
    revno = 1
110
108
    for revision_id in branch.revision_history():
111
109
        this_inv = branch.repository.get_inventory(revision_id)
112
 
        if file_id in this_inv:
 
110
        if this_inv.has_id(file_id):
113
111
            this_ie = this_inv[file_id]
114
112
            this_path = this_inv.id2path(file_id)
115
113
        else:
155
153
             end_revision=None,
156
154
             search=None,
157
155
             limit=None,
158
 
             show_diff=False):
 
156
             show_diff=False,
 
157
             match=None):
159
158
    """Write out human-readable log of commits to this branch.
160
159
 
161
160
    This function is being retained for backwards compatibility but
184
183
        if None or 0.
185
184
 
186
185
    :param show_diff: If True, output a diff after each revision.
 
186
 
 
187
    :param match: Dictionary of search lists to use when matching revision
 
188
      properties.
187
189
    """
188
190
    # Convert old-style parameters to new-style parameters
189
191
    if specific_fileid is not None:
213
215
    Logger(branch, rqst).show(lf)
214
216
 
215
217
 
216
 
# Note: This needs to be kept this in sync with the defaults in
 
218
# Note: This needs to be kept in sync with the defaults in
217
219
# make_log_request_dict() below
218
220
_DEFAULT_REQUEST_PARAMS = {
219
221
    'direction': 'reverse',
220
 
    'levels': 1,
 
222
    'levels': None,
221
223
    'generate_tags': True,
222
224
    'exclude_common_ancestry': False,
223
225
    '_match_using_deltas': True,
226
228
 
227
229
def make_log_request_dict(direction='reverse', specific_fileids=None,
228
230
                          start_revision=None, end_revision=None, limit=None,
229
 
                          message_search=None, levels=1, generate_tags=True,
 
231
                          message_search=None, levels=None, generate_tags=True,
230
232
                          delta_type=None,
231
233
                          diff_type=None, _match_using_deltas=True,
232
 
                          exclude_common_ancestry=False,
 
234
                          exclude_common_ancestry=False, match=None,
 
235
                          signature=False, omit_merges=False,
233
236
                          ):
234
237
    """Convenience function for making a logging request dictionary.
235
238
 
256
259
      matching commit messages
257
260
 
258
261
    :param levels: the number of levels of revisions to
259
 
      generate; 1 for just the mainline; 0 for all levels.
 
262
      generate; 1 for just the mainline; 0 for all levels, or None for
 
263
      a sensible default.
260
264
 
261
265
    :param generate_tags: If True, include tags for matched revisions.
262
 
 
 
266
`
263
267
    :param delta_type: Either 'full', 'partial' or None.
264
268
      'full' means generate the complete delta - adds/deletes/modifies/etc;
265
269
      'partial' means filter the delta using specific_fileids;
277
281
 
278
282
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
279
283
      range operator or as a graph difference.
 
284
 
 
285
    :param signature: show digital signature information
 
286
 
 
287
    :param match: Dictionary of list of search strings to use when filtering
 
288
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
289
      the empty string to match any of the preceding properties.
 
290
 
 
291
    :param omit_merges: If True, commits with more than one parent are
 
292
      omitted.
 
293
 
280
294
    """
 
295
    # Take care of old style message_search parameter
 
296
    if message_search:
 
297
        if match:
 
298
            if 'message' in match:
 
299
                match['message'].append(message_search)
 
300
            else:
 
301
                match['message'] = [message_search]
 
302
        else:
 
303
            match={ 'message': [message_search] }
281
304
    return {
282
305
        'direction': direction,
283
306
        'specific_fileids': specific_fileids,
284
307
        'start_revision': start_revision,
285
308
        'end_revision': end_revision,
286
309
        'limit': limit,
287
 
        'message_search': message_search,
288
310
        'levels': levels,
289
311
        'generate_tags': generate_tags,
290
312
        'delta_type': delta_type,
291
313
        'diff_type': diff_type,
292
314
        'exclude_common_ancestry': exclude_common_ancestry,
 
315
        'signature': signature,
 
316
        'match': match,
 
317
        'omit_merges': omit_merges,
293
318
        # Add 'private' attributes for features that may be deprecated
294
319
        '_match_using_deltas': _match_using_deltas,
295
320
    }
303
328
    return result
304
329
 
305
330
 
 
331
def format_signature_validity(rev_id, repo):
 
332
    """get the signature validity
 
333
 
 
334
    :param rev_id: revision id to validate
 
335
    :param repo: repository of revision
 
336
    :return: human readable string to print to log
 
337
    """
 
338
    from bzrlib import gpg
 
339
 
 
340
    gpg_strategy = gpg.GPGStrategy(None)
 
341
    result = repo.verify_revision(rev_id, gpg_strategy)
 
342
    if result[0] == gpg.SIGNATURE_VALID:
 
343
        return "valid signature from {0}".format(result[1])
 
344
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
345
        return "unknown key {0}".format(result[1])
 
346
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
347
        return "invalid signature!"
 
348
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
349
        return "no signature"
 
350
 
 
351
 
306
352
class LogGenerator(object):
307
353
    """A generator of log revisions."""
308
354
 
353
399
        # Tweak the LogRequest based on what the LogFormatter can handle.
354
400
        # (There's no point generating stuff if the formatter can't display it.)
355
401
        rqst = self.rqst
356
 
        rqst['levels'] = lf.get_levels()
 
402
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
403
            # user didn't specify levels, use whatever the LF can handle:
 
404
            rqst['levels'] = lf.get_levels()
 
405
 
357
406
        if not getattr(lf, 'supports_tags', False):
358
407
            rqst['generate_tags'] = False
359
408
        if not getattr(lf, 'supports_delta', False):
360
409
            rqst['delta_type'] = None
361
410
        if not getattr(lf, 'supports_diff', False):
362
411
            rqst['diff_type'] = None
 
412
        if not getattr(lf, 'supports_signatures', False):
 
413
            rqst['signature'] = False
363
414
 
364
415
        # Find and print the interesting revisions
365
416
        generator = self._generator_factory(self.branch, rqst)
369
420
 
370
421
    def _generator_factory(self, branch, rqst):
371
422
        """Make the LogGenerator object to use.
372
 
        
 
423
 
373
424
        Subclasses may wish to override this.
374
425
        """
375
426
        return _DefaultLogGenerator(branch, rqst)
399
450
        levels = rqst.get('levels')
400
451
        limit = rqst.get('limit')
401
452
        diff_type = rqst.get('diff_type')
 
453
        show_signature = rqst.get('signature')
 
454
        omit_merges = rqst.get('omit_merges')
402
455
        log_count = 0
403
456
        revision_iterator = self._create_log_revision_iterator()
404
457
        for revs in revision_iterator:
406
459
                # 0 levels means show everything; merge_depth counts from 0
407
460
                if levels != 0 and merge_depth >= levels:
408
461
                    continue
 
462
                if omit_merges and len(rev.parent_ids) > 1:
 
463
                    continue
409
464
                if diff_type is None:
410
465
                    diff = None
411
466
                else:
412
467
                    diff = self._format_diff(rev, rev_id, diff_type)
 
468
                if show_signature:
 
469
                    signature = format_signature_validity(rev_id,
 
470
                                                self.branch.repository)
 
471
                else:
 
472
                    signature = None
413
473
                yield LogRevision(rev, revno, merge_depth, delta,
414
 
                    self.rev_tag_dict.get(rev_id), diff)
 
474
                    self.rev_tag_dict.get(rev_id), diff, signature)
415
475
                if limit:
416
476
                    log_count += 1
417
477
                    if log_count >= limit:
472
532
 
473
533
        # Apply the other filters
474
534
        return make_log_rev_iterator(self.branch, view_revisions,
475
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
535
            rqst.get('delta_type'), rqst.get('match'),
476
536
            file_ids=rqst.get('specific_fileids'),
477
537
            direction=rqst.get('direction'))
478
538
 
491
551
            rqst.get('specific_fileids')[0], view_revisions,
492
552
            include_merges=rqst.get('levels') != 1)
493
553
        return make_log_rev_iterator(self.branch, view_revisions,
494
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
554
            rqst.get('delta_type'), rqst.get('match'))
495
555
 
496
556
 
497
557
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
565
             a list of the same tuples.
506
566
    """
507
567
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
 
        raise errors.BzrCommandError(
509
 
            '--exclude-common-ancestry requires two different revisions')
 
568
        raise errors.BzrCommandError(gettext(
 
569
            '--exclude-common-ancestry requires two different revisions'))
510
570
    if direction not in ('reverse', 'forward'):
511
 
        raise ValueError('invalid direction %r' % direction)
 
571
        raise ValueError(gettext('invalid direction %r') % direction)
512
572
    br_revno, br_rev_id = branch.last_revision_info()
513
573
    if br_revno == 0:
514
574
        return []
555
615
        try:
556
616
            result = list(result)
557
617
        except _StartNotLinearAncestor:
558
 
            raise errors.BzrCommandError('Start revision not found in'
559
 
                ' left-hand history of end revision.')
 
618
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
619
                ' left-hand history of end revision.'))
560
620
    return result
561
621
 
562
622
 
601
661
        except _StartNotLinearAncestor:
602
662
            # A merge was never detected so the lower revision limit can't
603
663
            # be nested down somewhere
604
 
            raise errors.BzrCommandError('Start revision not found in'
605
 
                ' history of end revision.')
 
664
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
665
                ' history of end revision.'))
606
666
 
607
667
    # We exit the loop above because we encounter a revision with merges, from
608
668
    # this revision, we need to switch to _graph_view_revisions.
678
738
    """
679
739
    br_revno, br_rev_id = branch.last_revision_info()
680
740
    repo = branch.repository
 
741
    graph = repo.get_graph()
681
742
    if start_rev_id is None and end_rev_id is None:
682
743
        cur_revno = br_revno
683
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
744
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
745
            (_mod_revision.NULL_REVISION,)):
684
746
            yield revision_id, str(cur_revno), 0
685
747
            cur_revno -= 1
686
748
    else:
687
749
        if end_rev_id is None:
688
750
            end_rev_id = br_rev_id
689
751
        found_start = start_rev_id is None
690
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
 
752
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
753
                (_mod_revision.NULL_REVISION,)):
691
754
            revno_str = _compute_revno_str(branch, revision_id)
692
755
            if not found_start and revision_id == start_rev_id:
693
756
                if not exclude_common_ancestry:
745
808
            yield rev_id, '.'.join(map(str, revno)), merge_depth
746
809
 
747
810
 
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
811
def _rebase_merge_depth(view_revisions):
768
812
    """Adjust depths upwards so the top level is 0."""
769
813
    # If either the first or last revision have a merge_depth of 0, we're done
813
857
    return log_rev_iterator
814
858
 
815
859
 
816
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
860
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
817
861
    """Create a filtered iterator of log_rev_iterator matching on a regex.
818
862
 
819
863
    :param branch: The branch being logged.
820
864
    :param generate_delta: Whether to generate a delta for each revision.
821
 
    :param search: A user text search string.
 
865
    :param match: A dictionary with properties as keys and lists of strings
 
866
        as values. To match, a revision may match any of the supplied strings
 
867
        within a single property but must match at least one string for each
 
868
        property.
822
869
    :param log_rev_iterator: An input iterator containing all revisions that
823
870
        could be displayed, in lists.
824
871
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
825
872
        delta).
826
873
    """
827
 
    if search is None:
 
874
    if match is None:
828
875
        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):
 
876
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
877
                for (k,v) in match.iteritems()]
 
878
    return _filter_re(searchRE, log_rev_iterator)
 
879
 
 
880
 
 
881
def _filter_re(searchRE, log_rev_iterator):
834
882
    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
 
 
 
883
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
884
        if new_revs:
 
885
            yield new_revs
 
886
 
 
887
def _match_filter(searchRE, rev):
 
888
    strings = {
 
889
               'message': (rev.message,),
 
890
               'committer': (rev.committer,),
 
891
               'author': (rev.get_apparent_authors()),
 
892
               'bugs': list(rev.iter_bugs())
 
893
               }
 
894
    strings[''] = [item for inner_list in strings.itervalues()
 
895
                   for item in inner_list]
 
896
    for (k,v) in searchRE:
 
897
        if k in strings and not _match_any_filter(strings[k], v):
 
898
            return False
 
899
    return True
 
900
 
 
901
def _match_any_filter(strings, res):
 
902
    return any([filter(None, map(re.search, strings)) for re in res])
841
903
 
842
904
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
843
905
    fileids=None, direction='reverse'):
916
978
 
917
979
def _update_fileids(delta, fileids, stop_on):
918
980
    """Update the set of file-ids to search based on file lifecycle events.
919
 
    
 
981
 
920
982
    :param fileids: a set of fileids to update
921
983
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
922
984
      fileids set once their add or remove entry is detected respectively
963
1025
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
964
1026
        delta).
965
1027
    """
966
 
    repository = branch.repository
967
1028
    num = 9
968
1029
    for batch in log_rev_iterator:
969
1030
        batch = iter(batch)
1018
1079
    if branch_revno != 0:
1019
1080
        if (start_rev_id == _mod_revision.NULL_REVISION
1020
1081
            or end_rev_id == _mod_revision.NULL_REVISION):
1021
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1082
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1022
1083
        if start_revno > end_revno:
1023
 
            raise errors.BzrCommandError("Start revision must be older than "
1024
 
                                         "the end revision.")
 
1084
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1085
                                         "older than the end revision."))
1025
1086
    return (start_rev_id, end_rev_id)
1026
1087
 
1027
1088
 
1076
1137
 
1077
1138
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1078
1139
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1079
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1140
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1080
1141
    if start_revno > end_revno:
1081
 
        raise errors.BzrCommandError("Start revision must be older than "
1082
 
                                     "the end revision.")
 
1142
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1143
                                     "than the end revision."))
1083
1144
 
1084
1145
    if end_revno < start_revno:
1085
1146
        return None, None, None, None
1086
1147
    cur_revno = branch_revno
1087
1148
    rev_nos = {}
1088
1149
    mainline_revs = []
1089
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1090
 
                        branch_last_revision):
 
1150
    graph = branch.repository.get_graph()
 
1151
    for revision_id in graph.iter_lefthand_ancestry(
 
1152
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1091
1153
        if cur_revno < start_revno:
1092
1154
            # We have gone far enough, but we always add 1 more revision
1093
1155
            rev_nos[revision_id] = cur_revno
1107
1169
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1108
1170
 
1109
1171
 
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
1172
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1155
1173
    include_merges=True):
1156
1174
    r"""Return the list of revision ids which touch a given file id.
1159
1177
    This includes the revisions which directly change the file id,
1160
1178
    and the revisions which merge these changes. So if the
1161
1179
    revision graph is::
 
1180
 
1162
1181
        A-.
1163
1182
        |\ \
1164
1183
        B C E
1191
1210
    """
1192
1211
    # Lookup all possible text keys to determine which ones actually modified
1193
1212
    # the file.
 
1213
    graph = branch.repository.get_file_graph()
 
1214
    get_parent_map = graph.get_parent_map
1194
1215
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1195
1216
    next_keys = None
1196
1217
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1200
1221
    #       indexing layer. We might consider passing in hints as to the known
1201
1222
    #       access pattern (sparse/clustered, high success rate/low success
1202
1223
    #       rate). This particular access is clustered with a low success rate.
1203
 
    get_parent_map = branch.repository.texts.get_parent_map
1204
1224
    modified_text_revisions = set()
1205
1225
    chunk_size = 1000
1206
1226
    for start in xrange(0, len(text_keys), chunk_size):
1233
1253
    return result
1234
1254
 
1235
1255
 
1236
 
@deprecated_function(deprecated_in((2, 2, 0)))
1237
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1238
 
                       include_merges=True):
1239
 
    """Produce an iterator of revisions to show
1240
 
    :return: an iterator of (revision_id, revno, merge_depth)
1241
 
    (if there is no revno for a revision, None is supplied)
1242
 
    """
1243
 
    if not include_merges:
1244
 
        revision_ids = mainline_revs[1:]
1245
 
        if direction == 'reverse':
1246
 
            revision_ids.reverse()
1247
 
        for revision_id in revision_ids:
1248
 
            yield revision_id, str(rev_nos[revision_id]), 0
1249
 
        return
1250
 
    graph = branch.repository.get_graph()
1251
 
    # This asks for all mainline revisions, which means we only have to spider
1252
 
    # sideways, rather than depth history. That said, its still size-of-history
1253
 
    # and should be addressed.
1254
 
    # mainline_revisions always includes an extra revision at the beginning, so
1255
 
    # don't request it.
1256
 
    parent_map = dict(((key, value) for key, value in
1257
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1258
 
    # filter out ghosts; merge_sort errors on ghosts.
1259
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1260
 
    merge_sorted_revisions = tsort.merge_sort(
1261
 
        rev_graph,
1262
 
        mainline_revs[-1],
1263
 
        mainline_revs,
1264
 
        generate_revno=True)
1265
 
 
1266
 
    if direction == 'forward':
1267
 
        # forward means oldest first.
1268
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1269
 
    elif direction != 'reverse':
1270
 
        raise ValueError('invalid direction %r' % direction)
1271
 
 
1272
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1273
 
         ) in merge_sorted_revisions:
1274
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1275
 
 
1276
 
 
1277
1256
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1278
1257
    """Reverse revisions by depth.
1279
1258
 
1314
1293
    """
1315
1294
 
1316
1295
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1317
 
                 tags=None, diff=None):
 
1296
                 tags=None, diff=None, signature=None):
1318
1297
        self.rev = rev
1319
1298
        if revno is None:
1320
1299
            self.revno = None
1324
1303
        self.delta = delta
1325
1304
        self.tags = tags
1326
1305
        self.diff = diff
 
1306
        self.signature = signature
1327
1307
 
1328
1308
 
1329
1309
class LogFormatter(object):
1338
1318
    to indicate which LogRevision attributes it supports:
1339
1319
 
1340
1320
    - supports_delta must be True if this log formatter supports delta.
1341
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1342
 
        attribute describes whether the 'short_status' format (1) or the long
1343
 
        one (2) should be used.
 
1321
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1322
      attribute describes whether the 'short_status' format (1) or the long
 
1323
      one (2) should be used.
1344
1324
 
1345
1325
    - supports_merge_revisions must be True if this log formatter supports
1346
 
        merge revisions.  If not, then only mainline revisions will be passed
1347
 
        to the formatter.
 
1326
      merge revisions.  If not, then only mainline revisions will be passed
 
1327
      to the formatter.
1348
1328
 
1349
1329
    - preferred_levels is the number of levels this formatter defaults to.
1350
 
        The default value is zero meaning display all levels.
1351
 
        This value is only relevant if supports_merge_revisions is True.
 
1330
      The default value is zero meaning display all levels.
 
1331
      This value is only relevant if supports_merge_revisions is True.
1352
1332
 
1353
1333
    - supports_tags must be True if this log formatter supports tags.
1354
 
        Otherwise the tags attribute may not be populated.
 
1334
      Otherwise the tags attribute may not be populated.
1355
1335
 
1356
1336
    - supports_diff must be True if this log formatter supports diffs.
1357
 
        Otherwise the diff attribute may not be populated.
 
1337
      Otherwise the diff attribute may not be populated.
 
1338
 
 
1339
    - supports_signatures must be True if this log formatter supports GPG
 
1340
      signatures.
1358
1341
 
1359
1342
    Plugins can register functions to show custom revision properties using
1360
1343
    the properties_handler_registry. The registered function
1361
 
    must respect the following interface description:
 
1344
    must respect the following interface description::
 
1345
 
1362
1346
        def my_show_properties(properties_dict):
1363
1347
            # code that returns a dict {'name':'value'} of the properties
1364
1348
            # to be shown
1371
1355
        """Create a LogFormatter.
1372
1356
 
1373
1357
        :param to_file: the file to output to
1374
 
        :param to_exact_file: if set, gives an output stream to which 
 
1358
        :param to_exact_file: if set, gives an output stream to which
1375
1359
             non-Unicode diffs are written.
1376
1360
        :param show_ids: if True, revision-ids are to be displayed
1377
1361
        :param show_timezone: the timezone to use
1428
1412
            if advice_sep:
1429
1413
                self.to_file.write(advice_sep)
1430
1414
            self.to_file.write(
1431
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1415
                "Use --include-merged or -n0 to see merged revisions.\n")
1432
1416
 
1433
1417
    def get_advice_separator(self):
1434
1418
        """Get the text separating the log from the closing advice."""
1551
1535
    supports_delta = True
1552
1536
    supports_tags = True
1553
1537
    supports_diff = True
 
1538
    supports_signatures = True
1554
1539
 
1555
1540
    def __init__(self, *args, **kwargs):
1556
1541
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1595
1580
 
1596
1581
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1597
1582
 
 
1583
        if revision.signature is not None:
 
1584
            lines.append('signature: ' + revision.signature)
 
1585
 
1598
1586
        lines.append('message:')
1599
1587
        if not revision.rev.message:
1600
1588
            lines.append('  (no message)')
1609
1597
        if revision.delta is not None:
1610
1598
            # Use the standard status output to display changes
1611
1599
            from bzrlib.delta import report_delta
1612
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1600
            report_delta(to_file, revision.delta, short_status=False,
1613
1601
                         show_ids=self.show_ids, indent=indent)
1614
1602
        if revision.diff is not None:
1615
1603
            to_file.write(indent + 'diff:\n')
1681
1669
        if revision.delta is not None:
1682
1670
            # Use the standard status output to display changes
1683
1671
            from bzrlib.delta import report_delta
1684
 
            report_delta(to_file, revision.delta, 
1685
 
                         short_status=self.delta_format==1, 
 
1672
            report_delta(to_file, revision.delta,
 
1673
                         short_status=self.delta_format==1,
1686
1674
                         show_ids=self.show_ids, indent=indent + offset)
1687
1675
        if revision.diff is not None:
1688
1676
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1727
1715
 
1728
1716
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1729
1717
        """Format log info into one string. Truncate tail of string
1730
 
        :param  revno:      revision number or None.
1731
 
                            Revision numbers counts from 1.
1732
 
        :param  rev:        revision object
1733
 
        :param  max_chars:  maximum length of resulting string
1734
 
        :param  tags:       list of tags or None
1735
 
        :param  prefix:     string to prefix each line
1736
 
        :return:            formatted truncated string
 
1718
 
 
1719
        :param revno:      revision number or None.
 
1720
                           Revision numbers counts from 1.
 
1721
        :param rev:        revision object
 
1722
        :param max_chars:  maximum length of resulting string
 
1723
        :param tags:       list of tags or None
 
1724
        :param prefix:     string to prefix each line
 
1725
        :return:           formatted truncated string
1737
1726
        """
1738
1727
        out = []
1739
1728
        if revno:
1740
1729
            # show revno only when is not None
1741
1730
            out.append("%s:" % revno)
1742
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1731
        if max_chars is not None:
 
1732
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1733
        else:
 
1734
            out.append(self.short_author(rev))
1743
1735
        out.append(self.date_string(rev))
1744
1736
        if len(rev.parent_ids) > 1:
1745
1737
            out.append('[merge]')
1833
1825
    try:
1834
1826
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1835
1827
    except KeyError:
1836
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1828
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1837
1829
 
1838
1830
 
1839
1831
def author_list_all(rev):
1864
1856
                              'The committer')
1865
1857
 
1866
1858
 
1867
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1868
 
    # deprecated; for compatibility
1869
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1870
 
    lf.show(revno, rev, delta)
1871
 
 
1872
 
 
1873
1859
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1874
1860
                           log_format='long'):
1875
1861
    """Show the change in revision history comparing the old revision history to the new one.
1938
1924
    old_revisions = set()
1939
1925
    new_history = []
1940
1926
    new_revisions = set()
1941
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1942
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1927
    graph = repository.get_graph()
 
1928
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1929
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1943
1930
    stop_revision = None
1944
1931
    do_old = True
1945
1932
    do_new = True
2133
2120
                          len(row) > 1 and row[1] == 'fixed']
2134
2121
 
2135
2122
        if fixed_bug_urls:
2136
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2123
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2124
                    ' '.join(fixed_bug_urls)}
2137
2125
    return {}
2138
2126
 
2139
2127
properties_handler_registry.register('bugs_properties_handler',