/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Vincent Ladeuil
  • Date: 2011-11-24 15:48:29 UTC
  • mfrom: (6289 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6337.
  • Revision ID: v.ladeuil+lp@free.fr-20111124154829-avowjpsxdl8yp2vz
merge trunk resolving conflicts

Show diffs side-by-side

added added

removed removed

Lines of Context:
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,
74
74
    revision as _mod_revision,
75
75
    revisionspec,
76
76
    tsort,
77
 
    i18n,
78
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
79
79
""")
80
80
 
81
81
from bzrlib import (
89
89
    get_terminal_encoding,
90
90
    terminal_width,
91
91
    )
92
 
from bzrlib.symbol_versioning import (
93
 
    deprecated_function,
94
 
    deprecated_in,
95
 
    )
96
92
 
97
93
 
98
94
def find_touching_revisions(branch, file_id):
109
105
    last_ie = None
110
106
    last_path = None
111
107
    revno = 1
112
 
    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):
113
112
        this_inv = branch.repository.get_inventory(revision_id)
114
113
        if this_inv.has_id(file_id):
115
114
            this_ie = this_inv[file_id]
157
156
             end_revision=None,
158
157
             search=None,
159
158
             limit=None,
160
 
             show_diff=False):
 
159
             show_diff=False,
 
160
             match=None):
161
161
    """Write out human-readable log of commits to this branch.
162
162
 
163
163
    This function is being retained for backwards compatibility but
186
186
        if None or 0.
187
187
 
188
188
    :param show_diff: If True, output a diff after each revision.
 
189
 
 
190
    :param match: Dictionary of search lists to use when matching revision
 
191
      properties.
189
192
    """
190
193
    # Convert old-style parameters to new-style parameters
191
194
    if specific_fileid is not None:
215
218
    Logger(branch, rqst).show(lf)
216
219
 
217
220
 
218
 
# Note: This needs to be kept this in sync with the defaults in
 
221
# Note: This needs to be kept in sync with the defaults in
219
222
# make_log_request_dict() below
220
223
_DEFAULT_REQUEST_PARAMS = {
221
224
    'direction': 'reverse',
222
 
    'levels': 1,
 
225
    'levels': None,
223
226
    'generate_tags': True,
224
227
    'exclude_common_ancestry': False,
225
228
    '_match_using_deltas': True,
228
231
 
229
232
def make_log_request_dict(direction='reverse', specific_fileids=None,
230
233
                          start_revision=None, end_revision=None, limit=None,
231
 
                          message_search=None, levels=1, generate_tags=True,
 
234
                          message_search=None, levels=None, generate_tags=True,
232
235
                          delta_type=None,
233
236
                          diff_type=None, _match_using_deltas=True,
234
 
                          exclude_common_ancestry=False,
235
 
                          signature=False,
 
237
                          exclude_common_ancestry=False, match=None,
 
238
                          signature=False, omit_merges=False,
236
239
                          ):
237
240
    """Convenience function for making a logging request dictionary.
238
241
 
259
262
      matching commit messages
260
263
 
261
264
    :param levels: the number of levels of revisions to
262
 
      generate; 1 for just the mainline; 0 for all levels.
 
265
      generate; 1 for just the mainline; 0 for all levels, or None for
 
266
      a sensible default.
263
267
 
264
268
    :param generate_tags: If True, include tags for matched revisions.
265
269
`
282
286
      range operator or as a graph difference.
283
287
 
284
288
    :param signature: show digital signature information
 
289
 
 
290
    :param match: Dictionary of list of search strings to use when filtering
 
291
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
292
      the empty string to match any of the preceding properties.
 
293
 
 
294
    :param omit_merges: If True, commits with more than one parent are
 
295
      omitted.
 
296
 
285
297
    """
 
298
    # Take care of old style message_search parameter
 
299
    if message_search:
 
300
        if match:
 
301
            if 'message' in match:
 
302
                match['message'].append(message_search)
 
303
            else:
 
304
                match['message'] = [message_search]
 
305
        else:
 
306
            match={ 'message': [message_search] }
286
307
    return {
287
308
        'direction': direction,
288
309
        'specific_fileids': specific_fileids,
289
310
        'start_revision': start_revision,
290
311
        'end_revision': end_revision,
291
312
        'limit': limit,
292
 
        'message_search': message_search,
293
313
        'levels': levels,
294
314
        'generate_tags': generate_tags,
295
315
        'delta_type': delta_type,
296
316
        'diff_type': diff_type,
297
317
        'exclude_common_ancestry': exclude_common_ancestry,
298
318
        'signature': signature,
 
319
        'match': match,
 
320
        'omit_merges': omit_merges,
299
321
        # Add 'private' attributes for features that may be deprecated
300
322
        '_match_using_deltas': _match_using_deltas,
301
323
    }
311
333
 
312
334
def format_signature_validity(rev_id, repo):
313
335
    """get the signature validity
314
 
    
 
336
 
315
337
    :param rev_id: revision id to validate
316
338
    :param repo: repository of revision
317
339
    :return: human readable string to print to log
319
341
    from bzrlib import gpg
320
342
 
321
343
    gpg_strategy = gpg.GPGStrategy(None)
322
 
    result = repo.verify_revision(rev_id, gpg_strategy)
 
344
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
323
345
    if result[0] == gpg.SIGNATURE_VALID:
324
346
        return "valid signature from {0}".format(result[1])
325
347
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
380
402
        # Tweak the LogRequest based on what the LogFormatter can handle.
381
403
        # (There's no point generating stuff if the formatter can't display it.)
382
404
        rqst = self.rqst
383
 
        rqst['levels'] = lf.get_levels()
 
405
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
406
            # user didn't specify levels, use whatever the LF can handle:
 
407
            rqst['levels'] = lf.get_levels()
 
408
 
384
409
        if not getattr(lf, 'supports_tags', False):
385
410
            rqst['generate_tags'] = False
386
411
        if not getattr(lf, 'supports_delta', False):
398
423
 
399
424
    def _generator_factory(self, branch, rqst):
400
425
        """Make the LogGenerator object to use.
401
 
        
 
426
 
402
427
        Subclasses may wish to override this.
403
428
        """
404
429
        return _DefaultLogGenerator(branch, rqst)
429
454
        limit = rqst.get('limit')
430
455
        diff_type = rqst.get('diff_type')
431
456
        show_signature = rqst.get('signature')
 
457
        omit_merges = rqst.get('omit_merges')
432
458
        log_count = 0
433
459
        revision_iterator = self._create_log_revision_iterator()
434
460
        for revs in revision_iterator:
436
462
                # 0 levels means show everything; merge_depth counts from 0
437
463
                if levels != 0 and merge_depth >= levels:
438
464
                    continue
 
465
                if omit_merges and len(rev.parent_ids) > 1:
 
466
                    continue
439
467
                if diff_type is None:
440
468
                    diff = None
441
469
                else:
507
535
 
508
536
        # Apply the other filters
509
537
        return make_log_rev_iterator(self.branch, view_revisions,
510
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
538
            rqst.get('delta_type'), rqst.get('match'),
511
539
            file_ids=rqst.get('specific_fileids'),
512
540
            direction=rqst.get('direction'))
513
541
 
526
554
            rqst.get('specific_fileids')[0], view_revisions,
527
555
            include_merges=rqst.get('levels') != 1)
528
556
        return make_log_rev_iterator(self.branch, view_revisions,
529
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
557
            rqst.get('delta_type'), rqst.get('match'))
530
558
 
531
559
 
532
560
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
540
568
             a list of the same tuples.
541
569
    """
542
570
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
543
 
        raise errors.BzrCommandError(
544
 
            '--exclude-common-ancestry requires two different revisions')
 
571
        raise errors.BzrCommandError(gettext(
 
572
            '--exclude-common-ancestry requires two different revisions'))
545
573
    if direction not in ('reverse', 'forward'):
546
 
        raise ValueError('invalid direction %r' % direction)
 
574
        raise ValueError(gettext('invalid direction %r') % direction)
547
575
    br_revno, br_rev_id = branch.last_revision_info()
548
576
    if br_revno == 0:
549
577
        return []
590
618
        try:
591
619
            result = list(result)
592
620
        except _StartNotLinearAncestor:
593
 
            raise errors.BzrCommandError('Start revision not found in'
594
 
                ' left-hand history of end revision.')
 
621
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
622
                ' left-hand history of end revision.'))
595
623
    return result
596
624
 
597
625
 
636
664
        except _StartNotLinearAncestor:
637
665
            # A merge was never detected so the lower revision limit can't
638
666
            # be nested down somewhere
639
 
            raise errors.BzrCommandError('Start revision not found in'
640
 
                ' history of end revision.')
 
667
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
668
                ' history of end revision.'))
641
669
 
642
670
    # We exit the loop above because we encounter a revision with merges, from
643
671
    # this revision, we need to switch to _graph_view_revisions.
783
811
            yield rev_id, '.'.join(map(str, revno)), merge_depth
784
812
 
785
813
 
786
 
@deprecated_function(deprecated_in((2, 2, 0)))
787
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
788
 
        specific_fileid, generate_merge_revisions):
789
 
    """Calculate the revisions to view.
790
 
 
791
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
792
 
             a list of the same tuples.
793
 
    """
794
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
795
 
        end_revision)
796
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
797
 
        direction, generate_merge_revisions or specific_fileid))
798
 
    if specific_fileid:
799
 
        view_revisions = _filter_revisions_touching_file_id(branch,
800
 
            specific_fileid, view_revisions,
801
 
            include_merges=generate_merge_revisions)
802
 
    return _rebase_merge_depth(view_revisions)
803
 
 
804
 
 
805
814
def _rebase_merge_depth(view_revisions):
806
815
    """Adjust depths upwards so the top level is 0."""
807
816
    # If either the first or last revision have a merge_depth of 0, we're done
851
860
    return log_rev_iterator
852
861
 
853
862
 
854
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
863
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
855
864
    """Create a filtered iterator of log_rev_iterator matching on a regex.
856
865
 
857
866
    :param branch: The branch being logged.
858
867
    :param generate_delta: Whether to generate a delta for each revision.
859
 
    :param search: A user text search string.
 
868
    :param match: A dictionary with properties as keys and lists of strings
 
869
        as values. To match, a revision may match any of the supplied strings
 
870
        within a single property but must match at least one string for each
 
871
        property.
860
872
    :param log_rev_iterator: An input iterator containing all revisions that
861
873
        could be displayed, in lists.
862
874
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
863
875
        delta).
864
876
    """
865
 
    if search is None:
 
877
    if match is None:
866
878
        return log_rev_iterator
867
 
    searchRE = lazy_regex.lazy_compile(search, re.IGNORECASE)
868
 
    return _filter_message_re(searchRE, log_rev_iterator)
869
 
 
870
 
 
871
 
def _filter_message_re(searchRE, log_rev_iterator):
 
879
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
880
                for (k,v) in match.iteritems()]
 
881
    return _filter_re(searchRE, log_rev_iterator)
 
882
 
 
883
 
 
884
def _filter_re(searchRE, log_rev_iterator):
872
885
    for revs in log_rev_iterator:
873
 
        new_revs = []
874
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
875
 
            if searchRE.search(rev.message):
876
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
877
 
        yield new_revs
878
 
 
 
886
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
887
        if new_revs:
 
888
            yield new_revs
 
889
 
 
890
def _match_filter(searchRE, rev):
 
891
    strings = {
 
892
               'message': (rev.message,),
 
893
               'committer': (rev.committer,),
 
894
               'author': (rev.get_apparent_authors()),
 
895
               'bugs': list(rev.iter_bugs())
 
896
               }
 
897
    strings[''] = [item for inner_list in strings.itervalues()
 
898
                   for item in inner_list]
 
899
    for (k,v) in searchRE:
 
900
        if k in strings and not _match_any_filter(strings[k], v):
 
901
            return False
 
902
    return True
 
903
 
 
904
def _match_any_filter(strings, res):
 
905
    return any([filter(None, map(re.search, strings)) for re in res])
879
906
 
880
907
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
881
908
    fileids=None, direction='reverse'):
954
981
 
955
982
def _update_fileids(delta, fileids, stop_on):
956
983
    """Update the set of file-ids to search based on file lifecycle events.
957
 
    
 
984
 
958
985
    :param fileids: a set of fileids to update
959
986
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
960
987
      fileids set once their add or remove entry is detected respectively
1001
1028
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1002
1029
        delta).
1003
1030
    """
1004
 
    repository = branch.repository
1005
1031
    num = 9
1006
1032
    for batch in log_rev_iterator:
1007
1033
        batch = iter(batch)
1056
1082
    if branch_revno != 0:
1057
1083
        if (start_rev_id == _mod_revision.NULL_REVISION
1058
1084
            or end_rev_id == _mod_revision.NULL_REVISION):
1059
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1085
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1060
1086
        if start_revno > end_revno:
1061
 
            raise errors.BzrCommandError("Start revision must be older than "
1062
 
                                         "the end revision.")
 
1087
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1088
                                         "older than the end revision."))
1063
1089
    return (start_rev_id, end_rev_id)
1064
1090
 
1065
1091
 
1114
1140
 
1115
1141
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1116
1142
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1117
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1143
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1118
1144
    if start_revno > end_revno:
1119
 
        raise errors.BzrCommandError("Start revision must be older than "
1120
 
                                     "the end revision.")
 
1145
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1146
                                     "than the end revision."))
1121
1147
 
1122
1148
    if end_revno < start_revno:
1123
1149
        return None, None, None, None
1146
1172
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1147
1173
 
1148
1174
 
1149
 
@deprecated_function(deprecated_in((2, 2, 0)))
1150
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1151
 
    """Filter view_revisions based on revision ranges.
1152
 
 
1153
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1154
 
            tuples to be filtered.
1155
 
 
1156
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1157
 
            If NONE then all revisions up to the end_rev_id are logged.
1158
 
 
1159
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1160
 
            If NONE then all revisions up to the end of the log are logged.
1161
 
 
1162
 
    :return: The filtered view_revisions.
1163
 
    """
1164
 
    if start_rev_id or end_rev_id:
1165
 
        revision_ids = [r for r, n, d in view_revisions]
1166
 
        if start_rev_id:
1167
 
            start_index = revision_ids.index(start_rev_id)
1168
 
        else:
1169
 
            start_index = 0
1170
 
        if start_rev_id == end_rev_id:
1171
 
            end_index = start_index
1172
 
        else:
1173
 
            if end_rev_id:
1174
 
                end_index = revision_ids.index(end_rev_id)
1175
 
            else:
1176
 
                end_index = len(view_revisions) - 1
1177
 
        # To include the revisions merged into the last revision,
1178
 
        # extend end_rev_id down to, but not including, the next rev
1179
 
        # with the same or lesser merge_depth
1180
 
        end_merge_depth = view_revisions[end_index][2]
1181
 
        try:
1182
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1183
 
                if view_revisions[index][2] <= end_merge_depth:
1184
 
                    end_index = index - 1
1185
 
                    break
1186
 
        except IndexError:
1187
 
            # if the search falls off the end then log to the end as well
1188
 
            end_index = len(view_revisions) - 1
1189
 
        view_revisions = view_revisions[start_index:end_index+1]
1190
 
    return view_revisions
1191
 
 
1192
 
 
1193
1175
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1194
1176
    include_merges=True):
1195
1177
    r"""Return the list of revision ids which touch a given file id.
1274
1256
    return result
1275
1257
 
1276
1258
 
1277
 
@deprecated_function(deprecated_in((2, 2, 0)))
1278
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1279
 
                       include_merges=True):
1280
 
    """Produce an iterator of revisions to show
1281
 
    :return: an iterator of (revision_id, revno, merge_depth)
1282
 
    (if there is no revno for a revision, None is supplied)
1283
 
    """
1284
 
    if not include_merges:
1285
 
        revision_ids = mainline_revs[1:]
1286
 
        if direction == 'reverse':
1287
 
            revision_ids.reverse()
1288
 
        for revision_id in revision_ids:
1289
 
            yield revision_id, str(rev_nos[revision_id]), 0
1290
 
        return
1291
 
    graph = branch.repository.get_graph()
1292
 
    # This asks for all mainline revisions, which means we only have to spider
1293
 
    # sideways, rather than depth history. That said, its still size-of-history
1294
 
    # and should be addressed.
1295
 
    # mainline_revisions always includes an extra revision at the beginning, so
1296
 
    # don't request it.
1297
 
    parent_map = dict(((key, value) for key, value in
1298
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1299
 
    # filter out ghosts; merge_sort errors on ghosts.
1300
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1301
 
    merge_sorted_revisions = tsort.merge_sort(
1302
 
        rev_graph,
1303
 
        mainline_revs[-1],
1304
 
        mainline_revs,
1305
 
        generate_revno=True)
1306
 
 
1307
 
    if direction == 'forward':
1308
 
        # forward means oldest first.
1309
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1310
 
    elif direction != 'reverse':
1311
 
        raise ValueError('invalid direction %r' % direction)
1312
 
 
1313
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1314
 
         ) in merge_sorted_revisions:
1315
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1316
 
 
1317
 
 
1318
1259
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1319
1260
    """Reverse revisions by depth.
1320
1261
 
1417
1358
        """Create a LogFormatter.
1418
1359
 
1419
1360
        :param to_file: the file to output to
1420
 
        :param to_exact_file: if set, gives an output stream to which 
 
1361
        :param to_exact_file: if set, gives an output stream to which
1421
1362
             non-Unicode diffs are written.
1422
1363
        :param show_ids: if True, revision-ids are to be displayed
1423
1364
        :param show_timezone: the timezone to use
1474
1415
            if advice_sep:
1475
1416
                self.to_file.write(advice_sep)
1476
1417
            self.to_file.write(
1477
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1418
                "Use --include-merged or -n0 to see merged revisions.\n")
1478
1419
 
1479
1420
    def get_advice_separator(self):
1480
1421
        """Get the text separating the log from the closing advice."""
1659
1600
        if revision.delta is not None:
1660
1601
            # Use the standard status output to display changes
1661
1602
            from bzrlib.delta import report_delta
1662
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1603
            report_delta(to_file, revision.delta, short_status=False,
1663
1604
                         show_ids=self.show_ids, indent=indent)
1664
1605
        if revision.diff is not None:
1665
1606
            to_file.write(indent + 'diff:\n')
1731
1672
        if revision.delta is not None:
1732
1673
            # Use the standard status output to display changes
1733
1674
            from bzrlib.delta import report_delta
1734
 
            report_delta(to_file, revision.delta, 
1735
 
                         short_status=self.delta_format==1, 
 
1675
            report_delta(to_file, revision.delta,
 
1676
                         short_status=self.delta_format==1,
1736
1677
                         show_ids=self.show_ids, indent=indent + offset)
1737
1678
        if revision.diff is not None:
1738
1679
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1858
1799
        return self.get(name)(*args, **kwargs)
1859
1800
 
1860
1801
    def get_default(self, branch):
1861
 
        return self.get(branch.get_config().log_format())
 
1802
        c = branch.get_config_stack()
 
1803
        return self.get(c.get('log_format'))
1862
1804
 
1863
1805
 
1864
1806
log_formatter_registry = LogFormatterRegistry()
1887
1829
    try:
1888
1830
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1889
1831
    except KeyError:
1890
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1832
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1891
1833
 
1892
1834
 
1893
1835
def author_list_all(rev):
1918
1860
                              'The committer')
1919
1861
 
1920
1862
 
1921
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1922
 
    # deprecated; for compatibility
1923
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1924
 
    lf.show(revno, rev, delta)
1925
 
 
1926
 
 
1927
1863
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1928
1864
                           log_format='long'):
1929
1865
    """Show the change in revision history comparing the old revision history to the new one.
2093
2029
      branch will be read-locked.
2094
2030
    """
2095
2031
    from builtins import _get_revision_range
2096
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
2032
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2033
        file_list[0])
2097
2034
    add_cleanup(b.lock_read().unlock)
2098
2035
    # XXX: It's damn messy converting a list of paths to relative paths when
2099
2036
    # those paths might be deleted ones, they might be on a case-insensitive
2188
2125
                          len(row) > 1 and row[1] == 'fixed']
2189
2126
 
2190
2127
        if fixed_bug_urls:
2191
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2128
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2129
                    ' '.join(fixed_bug_urls)}
2192
2130
    return {}
2193
2131
 
2194
2132
properties_handler_registry.register('bugs_properties_handler',