~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jelmer Vernooij
  • Date: 2011-10-04 22:20:49 UTC
  • mto: This revision was merged to the branch mainline in revision 6190.
  • Revision ID: jelmer@samba.org-20111004222049-d9glniyleu0pppzd
Add a load_plugin_translations method.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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):
157
153
             end_revision=None,
158
154
             search=None,
159
155
             limit=None,
160
 
             show_diff=False):
 
156
             show_diff=False,
 
157
             match=None):
161
158
    """Write out human-readable log of commits to this branch.
162
159
 
163
160
    This function is being retained for backwards compatibility but
186
183
        if None or 0.
187
184
 
188
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.
189
189
    """
190
190
    # Convert old-style parameters to new-style parameters
191
191
    if specific_fileid is not None:
215
215
    Logger(branch, rqst).show(lf)
216
216
 
217
217
 
218
 
# 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
219
219
# make_log_request_dict() below
220
220
_DEFAULT_REQUEST_PARAMS = {
221
221
    'direction': 'reverse',
222
 
    'levels': 1,
 
222
    'levels': None,
223
223
    'generate_tags': True,
224
224
    'exclude_common_ancestry': False,
225
225
    '_match_using_deltas': True,
228
228
 
229
229
def make_log_request_dict(direction='reverse', specific_fileids=None,
230
230
                          start_revision=None, end_revision=None, limit=None,
231
 
                          message_search=None, levels=1, generate_tags=True,
 
231
                          message_search=None, levels=None, generate_tags=True,
232
232
                          delta_type=None,
233
233
                          diff_type=None, _match_using_deltas=True,
234
 
                          exclude_common_ancestry=False,
235
 
                          signature=False,
 
234
                          exclude_common_ancestry=False, match=None,
 
235
                          signature=False, omit_merges=False,
236
236
                          ):
237
237
    """Convenience function for making a logging request dictionary.
238
238
 
259
259
      matching commit messages
260
260
 
261
261
    :param levels: the number of levels of revisions to
262
 
      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.
263
264
 
264
265
    :param generate_tags: If True, include tags for matched revisions.
265
266
`
282
283
      range operator or as a graph difference.
283
284
 
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
 
285
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] }
286
304
    return {
287
305
        'direction': direction,
288
306
        'specific_fileids': specific_fileids,
289
307
        'start_revision': start_revision,
290
308
        'end_revision': end_revision,
291
309
        'limit': limit,
292
 
        'message_search': message_search,
293
310
        'levels': levels,
294
311
        'generate_tags': generate_tags,
295
312
        'delta_type': delta_type,
296
313
        'diff_type': diff_type,
297
314
        'exclude_common_ancestry': exclude_common_ancestry,
298
315
        'signature': signature,
 
316
        'match': match,
 
317
        'omit_merges': omit_merges,
299
318
        # Add 'private' attributes for features that may be deprecated
300
319
        '_match_using_deltas': _match_using_deltas,
301
320
    }
311
330
 
312
331
def format_signature_validity(rev_id, repo):
313
332
    """get the signature validity
314
 
    
 
333
 
315
334
    :param rev_id: revision id to validate
316
335
    :param repo: repository of revision
317
336
    :return: human readable string to print to log
380
399
        # Tweak the LogRequest based on what the LogFormatter can handle.
381
400
        # (There's no point generating stuff if the formatter can't display it.)
382
401
        rqst = self.rqst
383
 
        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
 
384
406
        if not getattr(lf, 'supports_tags', False):
385
407
            rqst['generate_tags'] = False
386
408
        if not getattr(lf, 'supports_delta', False):
398
420
 
399
421
    def _generator_factory(self, branch, rqst):
400
422
        """Make the LogGenerator object to use.
401
 
        
 
423
 
402
424
        Subclasses may wish to override this.
403
425
        """
404
426
        return _DefaultLogGenerator(branch, rqst)
429
451
        limit = rqst.get('limit')
430
452
        diff_type = rqst.get('diff_type')
431
453
        show_signature = rqst.get('signature')
 
454
        omit_merges = rqst.get('omit_merges')
432
455
        log_count = 0
433
456
        revision_iterator = self._create_log_revision_iterator()
434
457
        for revs in revision_iterator:
436
459
                # 0 levels means show everything; merge_depth counts from 0
437
460
                if levels != 0 and merge_depth >= levels:
438
461
                    continue
 
462
                if omit_merges and len(rev.parent_ids) > 1:
 
463
                    continue
439
464
                if diff_type is None:
440
465
                    diff = None
441
466
                else:
507
532
 
508
533
        # Apply the other filters
509
534
        return make_log_rev_iterator(self.branch, view_revisions,
510
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
535
            rqst.get('delta_type'), rqst.get('match'),
511
536
            file_ids=rqst.get('specific_fileids'),
512
537
            direction=rqst.get('direction'))
513
538
 
526
551
            rqst.get('specific_fileids')[0], view_revisions,
527
552
            include_merges=rqst.get('levels') != 1)
528
553
        return make_log_rev_iterator(self.branch, view_revisions,
529
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
554
            rqst.get('delta_type'), rqst.get('match'))
530
555
 
531
556
 
532
557
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
540
565
             a list of the same tuples.
541
566
    """
542
567
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
543
 
        raise errors.BzrCommandError(
544
 
            '--exclude-common-ancestry requires two different revisions')
 
568
        raise errors.BzrCommandError(gettext(
 
569
            '--exclude-common-ancestry requires two different revisions'))
545
570
    if direction not in ('reverse', 'forward'):
546
 
        raise ValueError('invalid direction %r' % direction)
 
571
        raise ValueError(gettext('invalid direction %r') % direction)
547
572
    br_revno, br_rev_id = branch.last_revision_info()
548
573
    if br_revno == 0:
549
574
        return []
590
615
        try:
591
616
            result = list(result)
592
617
        except _StartNotLinearAncestor:
593
 
            raise errors.BzrCommandError('Start revision not found in'
594
 
                ' left-hand history of end revision.')
 
618
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
619
                ' left-hand history of end revision.'))
595
620
    return result
596
621
 
597
622
 
636
661
        except _StartNotLinearAncestor:
637
662
            # A merge was never detected so the lower revision limit can't
638
663
            # be nested down somewhere
639
 
            raise errors.BzrCommandError('Start revision not found in'
640
 
                ' history of end revision.')
 
664
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
665
                ' history of end revision.'))
641
666
 
642
667
    # We exit the loop above because we encounter a revision with merges, from
643
668
    # this revision, we need to switch to _graph_view_revisions.
783
808
            yield rev_id, '.'.join(map(str, revno)), merge_depth
784
809
 
785
810
 
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
811
def _rebase_merge_depth(view_revisions):
806
812
    """Adjust depths upwards so the top level is 0."""
807
813
    # If either the first or last revision have a merge_depth of 0, we're done
851
857
    return log_rev_iterator
852
858
 
853
859
 
854
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
860
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
855
861
    """Create a filtered iterator of log_rev_iterator matching on a regex.
856
862
 
857
863
    :param branch: The branch being logged.
858
864
    :param generate_delta: Whether to generate a delta for each revision.
859
 
    :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.
860
869
    :param log_rev_iterator: An input iterator containing all revisions that
861
870
        could be displayed, in lists.
862
871
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
863
872
        delta).
864
873
    """
865
 
    if search is None:
 
874
    if match is None:
866
875
        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):
 
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):
872
882
    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
 
 
 
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])
879
903
 
880
904
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
881
905
    fileids=None, direction='reverse'):
954
978
 
955
979
def _update_fileids(delta, fileids, stop_on):
956
980
    """Update the set of file-ids to search based on file lifecycle events.
957
 
    
 
981
 
958
982
    :param fileids: a set of fileids to update
959
983
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
960
984
      fileids set once their add or remove entry is detected respectively
1001
1025
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1002
1026
        delta).
1003
1027
    """
1004
 
    repository = branch.repository
1005
1028
    num = 9
1006
1029
    for batch in log_rev_iterator:
1007
1030
        batch = iter(batch)
1056
1079
    if branch_revno != 0:
1057
1080
        if (start_rev_id == _mod_revision.NULL_REVISION
1058
1081
            or end_rev_id == _mod_revision.NULL_REVISION):
1059
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1082
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1060
1083
        if start_revno > end_revno:
1061
 
            raise errors.BzrCommandError("Start revision must be older than "
1062
 
                                         "the end revision.")
 
1084
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1085
                                         "older than the end revision."))
1063
1086
    return (start_rev_id, end_rev_id)
1064
1087
 
1065
1088
 
1114
1137
 
1115
1138
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1116
1139
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1117
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1140
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1118
1141
    if start_revno > end_revno:
1119
 
        raise errors.BzrCommandError("Start revision must be older than "
1120
 
                                     "the end revision.")
 
1142
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1143
                                     "than the end revision."))
1121
1144
 
1122
1145
    if end_revno < start_revno:
1123
1146
        return None, None, None, None
1146
1169
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1147
1170
 
1148
1171
 
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
1172
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1194
1173
    include_merges=True):
1195
1174
    r"""Return the list of revision ids which touch a given file id.
1274
1253
    return result
1275
1254
 
1276
1255
 
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
1256
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1319
1257
    """Reverse revisions by depth.
1320
1258
 
1417
1355
        """Create a LogFormatter.
1418
1356
 
1419
1357
        :param to_file: the file to output to
1420
 
        :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
1421
1359
             non-Unicode diffs are written.
1422
1360
        :param show_ids: if True, revision-ids are to be displayed
1423
1361
        :param show_timezone: the timezone to use
1474
1412
            if advice_sep:
1475
1413
                self.to_file.write(advice_sep)
1476
1414
            self.to_file.write(
1477
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1415
                "Use --include-merged or -n0 to see merged revisions.\n")
1478
1416
 
1479
1417
    def get_advice_separator(self):
1480
1418
        """Get the text separating the log from the closing advice."""
1659
1597
        if revision.delta is not None:
1660
1598
            # Use the standard status output to display changes
1661
1599
            from bzrlib.delta import report_delta
1662
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1600
            report_delta(to_file, revision.delta, short_status=False,
1663
1601
                         show_ids=self.show_ids, indent=indent)
1664
1602
        if revision.diff is not None:
1665
1603
            to_file.write(indent + 'diff:\n')
1731
1669
        if revision.delta is not None:
1732
1670
            # Use the standard status output to display changes
1733
1671
            from bzrlib.delta import report_delta
1734
 
            report_delta(to_file, revision.delta, 
1735
 
                         short_status=self.delta_format==1, 
 
1672
            report_delta(to_file, revision.delta,
 
1673
                         short_status=self.delta_format==1,
1736
1674
                         show_ids=self.show_ids, indent=indent + offset)
1737
1675
        if revision.diff is not None:
1738
1676
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1887
1825
    try:
1888
1826
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1889
1827
    except KeyError:
1890
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1828
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1891
1829
 
1892
1830
 
1893
1831
def author_list_all(rev):
1918
1856
                              'The committer')
1919
1857
 
1920
1858
 
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
1859
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1928
1860
                           log_format='long'):
1929
1861
    """Show the change in revision history comparing the old revision history to the new one.
2188
2120
                          len(row) > 1 and row[1] == 'fixed']
2189
2121
 
2190
2122
        if fixed_bug_urls:
2191
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2123
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2124
                    ' '.join(fixed_bug_urls)}
2192
2125
    return {}
2193
2126
 
2194
2127
properties_handler_registry.register('bugs_properties_handler',