~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-08-15 13:49:38 UTC
  • mfrom: (6060.3.1 fix-last-revno-args)
  • Revision ID: pqm@pqm.ubuntu.com-20110815134938-4fuo63g4v2hj8jdt
(jelmer) Cope with the localhost having the name 'localhost' when running
 the test suite. (Jelmer Vernooij)

Show diffs side-by-side

added added

removed removed

Lines of Context:
79
79
""")
80
80
 
81
81
from bzrlib import (
 
82
    lazy_regex,
82
83
    registry,
83
84
    )
84
85
from bzrlib.osutils import (
88
89
    get_terminal_encoding,
89
90
    terminal_width,
90
91
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
94
 
    )
95
92
 
96
93
 
97
94
def find_touching_revisions(branch, file_id):
110
107
    revno = 1
111
108
    for revision_id in branch.revision_history():
112
109
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if file_id in this_inv:
 
110
        if this_inv.has_id(file_id):
114
111
            this_ie = this_inv[file_id]
115
112
            this_path = this_inv.id2path(file_id)
116
113
        else:
156
153
             end_revision=None,
157
154
             search=None,
158
155
             limit=None,
159
 
             show_diff=False):
 
156
             show_diff=False,
 
157
             match=None):
160
158
    """Write out human-readable log of commits to this branch.
161
159
 
162
160
    This function is being retained for backwards compatibility but
185
183
        if None or 0.
186
184
 
187
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.
188
189
    """
189
190
    # Convert old-style parameters to new-style parameters
190
191
    if specific_fileid is not None:
218
219
# make_log_request_dict() below
219
220
_DEFAULT_REQUEST_PARAMS = {
220
221
    'direction': 'reverse',
221
 
    'levels': 1,
 
222
    'levels': None,
222
223
    'generate_tags': True,
223
224
    'exclude_common_ancestry': False,
224
225
    '_match_using_deltas': True,
227
228
 
228
229
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
230
                          start_revision=None, end_revision=None, limit=None,
230
 
                          message_search=None, levels=1, generate_tags=True,
 
231
                          message_search=None, levels=None, generate_tags=True,
231
232
                          delta_type=None,
232
233
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
 
234
                          exclude_common_ancestry=False, match=None,
234
235
                          signature=False,
235
236
                          ):
236
237
    """Convenience function for making a logging request dictionary.
258
259
      matching commit messages
259
260
 
260
261
    :param levels: the number of levels of revisions to
261
 
      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.
262
264
 
263
265
    :param generate_tags: If True, include tags for matched revisions.
264
266
`
281
283
      range operator or as a graph difference.
282
284
 
283
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
 
284
291
    """
 
292
    # Take care of old style message_search parameter
 
293
    if message_search:
 
294
        if match:
 
295
            if 'message' in match:
 
296
                match['message'].append(message_search)
 
297
            else:
 
298
                match['message'] = [message_search]
 
299
        else:
 
300
            match={ 'message': [message_search] }
285
301
    return {
286
302
        'direction': direction,
287
303
        'specific_fileids': specific_fileids,
288
304
        'start_revision': start_revision,
289
305
        'end_revision': end_revision,
290
306
        'limit': limit,
291
 
        'message_search': message_search,
292
307
        'levels': levels,
293
308
        'generate_tags': generate_tags,
294
309
        'delta_type': delta_type,
295
310
        'diff_type': diff_type,
296
311
        'exclude_common_ancestry': exclude_common_ancestry,
297
312
        'signature': signature,
 
313
        'match': match,
298
314
        # Add 'private' attributes for features that may be deprecated
299
315
        '_match_using_deltas': _match_using_deltas,
300
316
    }
310
326
 
311
327
def format_signature_validity(rev_id, repo):
312
328
    """get the signature validity
313
 
    
 
329
 
314
330
    :param rev_id: revision id to validate
315
331
    :param repo: repository of revision
316
332
    :return: human readable string to print to log
320
336
    gpg_strategy = gpg.GPGStrategy(None)
321
337
    result = repo.verify_revision(rev_id, gpg_strategy)
322
338
    if result[0] == gpg.SIGNATURE_VALID:
323
 
        return i18n.gettext("valid signature from {0}").format(result[1])
 
339
        return "valid signature from {0}".format(result[1])
324
340
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
325
 
        return i18n.gettext("unknown key {0}").format(result[1])
 
341
        return "unknown key {0}".format(result[1])
326
342
    if result[0] == gpg.SIGNATURE_NOT_VALID:
327
 
        return i18n.gettext("invalid signature!")
 
343
        return "invalid signature!"
328
344
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
329
 
        return i18n.gettext("no signature")
 
345
        return "no signature"
330
346
 
331
347
 
332
348
class LogGenerator(object):
379
395
        # Tweak the LogRequest based on what the LogFormatter can handle.
380
396
        # (There's no point generating stuff if the formatter can't display it.)
381
397
        rqst = self.rqst
382
 
        rqst['levels'] = lf.get_levels()
 
398
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
399
            # user didn't specify levels, use whatever the LF can handle:
 
400
            rqst['levels'] = lf.get_levels()
 
401
 
383
402
        if not getattr(lf, 'supports_tags', False):
384
403
            rqst['generate_tags'] = False
385
404
        if not getattr(lf, 'supports_delta', False):
397
416
 
398
417
    def _generator_factory(self, branch, rqst):
399
418
        """Make the LogGenerator object to use.
400
 
        
 
419
 
401
420
        Subclasses may wish to override this.
402
421
        """
403
422
        return _DefaultLogGenerator(branch, rqst)
506
525
 
507
526
        # Apply the other filters
508
527
        return make_log_rev_iterator(self.branch, view_revisions,
509
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
528
            rqst.get('delta_type'), rqst.get('match'),
510
529
            file_ids=rqst.get('specific_fileids'),
511
530
            direction=rqst.get('direction'))
512
531
 
525
544
            rqst.get('specific_fileids')[0], view_revisions,
526
545
            include_merges=rqst.get('levels') != 1)
527
546
        return make_log_rev_iterator(self.branch, view_revisions,
528
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
547
            rqst.get('delta_type'), rqst.get('match'))
529
548
 
530
549
 
531
550
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
782
801
            yield rev_id, '.'.join(map(str, revno)), merge_depth
783
802
 
784
803
 
785
 
@deprecated_function(deprecated_in((2, 2, 0)))
786
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
787
 
        specific_fileid, generate_merge_revisions):
788
 
    """Calculate the revisions to view.
789
 
 
790
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
791
 
             a list of the same tuples.
792
 
    """
793
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
794
 
        end_revision)
795
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
796
 
        direction, generate_merge_revisions or specific_fileid))
797
 
    if specific_fileid:
798
 
        view_revisions = _filter_revisions_touching_file_id(branch,
799
 
            specific_fileid, view_revisions,
800
 
            include_merges=generate_merge_revisions)
801
 
    return _rebase_merge_depth(view_revisions)
802
 
 
803
 
 
804
804
def _rebase_merge_depth(view_revisions):
805
805
    """Adjust depths upwards so the top level is 0."""
806
806
    # If either the first or last revision have a merge_depth of 0, we're done
850
850
    return log_rev_iterator
851
851
 
852
852
 
853
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
853
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
854
854
    """Create a filtered iterator of log_rev_iterator matching on a regex.
855
855
 
856
856
    :param branch: The branch being logged.
857
857
    :param generate_delta: Whether to generate a delta for each revision.
858
 
    :param search: A user text search string.
 
858
    :param match: A dictionary with properties as keys and lists of strings
 
859
        as values. To match, a revision may match any of the supplied strings
 
860
        within a single property but must match at least one string for each
 
861
        property.
859
862
    :param log_rev_iterator: An input iterator containing all revisions that
860
863
        could be displayed, in lists.
861
864
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
862
865
        delta).
863
866
    """
864
 
    if search is None:
 
867
    if match is None:
865
868
        return log_rev_iterator
866
 
    searchRE = re.compile(search, re.IGNORECASE)
867
 
    return _filter_message_re(searchRE, log_rev_iterator)
868
 
 
869
 
 
870
 
def _filter_message_re(searchRE, log_rev_iterator):
 
869
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
870
                for (k,v) in match.iteritems()]
 
871
    return _filter_re(searchRE, log_rev_iterator)
 
872
 
 
873
 
 
874
def _filter_re(searchRE, log_rev_iterator):
871
875
    for revs in log_rev_iterator:
872
 
        new_revs = []
873
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
874
 
            if searchRE.search(rev.message):
875
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
876
 
        yield new_revs
877
 
 
 
876
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
877
        if new_revs:
 
878
            yield new_revs
 
879
 
 
880
def _match_filter(searchRE, rev):
 
881
    strings = {
 
882
               'message': (rev.message,),
 
883
               'committer': (rev.committer,),
 
884
               'author': (rev.get_apparent_authors()),
 
885
               'bugs': list(rev.iter_bugs())
 
886
               }
 
887
    strings[''] = [item for inner_list in strings.itervalues()
 
888
                   for item in inner_list]
 
889
    for (k,v) in searchRE:
 
890
        if k in strings and not _match_any_filter(strings[k], v):
 
891
            return False
 
892
    return True
 
893
 
 
894
def _match_any_filter(strings, res):
 
895
    return any([filter(None, map(re.search, strings)) for re in res])
878
896
 
879
897
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
880
898
    fileids=None, direction='reverse'):
953
971
 
954
972
def _update_fileids(delta, fileids, stop_on):
955
973
    """Update the set of file-ids to search based on file lifecycle events.
956
 
    
 
974
 
957
975
    :param fileids: a set of fileids to update
958
976
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
959
977
      fileids set once their add or remove entry is detected respectively
1000
1018
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1001
1019
        delta).
1002
1020
    """
1003
 
    repository = branch.repository
1004
1021
    num = 9
1005
1022
    for batch in log_rev_iterator:
1006
1023
        batch = iter(batch)
1145
1162
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1146
1163
 
1147
1164
 
1148
 
@deprecated_function(deprecated_in((2, 2, 0)))
1149
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1150
 
    """Filter view_revisions based on revision ranges.
1151
 
 
1152
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1153
 
            tuples to be filtered.
1154
 
 
1155
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1156
 
            If NONE then all revisions up to the end_rev_id are logged.
1157
 
 
1158
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1159
 
            If NONE then all revisions up to the end of the log are logged.
1160
 
 
1161
 
    :return: The filtered view_revisions.
1162
 
    """
1163
 
    if start_rev_id or end_rev_id:
1164
 
        revision_ids = [r for r, n, d in view_revisions]
1165
 
        if start_rev_id:
1166
 
            start_index = revision_ids.index(start_rev_id)
1167
 
        else:
1168
 
            start_index = 0
1169
 
        if start_rev_id == end_rev_id:
1170
 
            end_index = start_index
1171
 
        else:
1172
 
            if end_rev_id:
1173
 
                end_index = revision_ids.index(end_rev_id)
1174
 
            else:
1175
 
                end_index = len(view_revisions) - 1
1176
 
        # To include the revisions merged into the last revision,
1177
 
        # extend end_rev_id down to, but not including, the next rev
1178
 
        # with the same or lesser merge_depth
1179
 
        end_merge_depth = view_revisions[end_index][2]
1180
 
        try:
1181
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1182
 
                if view_revisions[index][2] <= end_merge_depth:
1183
 
                    end_index = index - 1
1184
 
                    break
1185
 
        except IndexError:
1186
 
            # if the search falls off the end then log to the end as well
1187
 
            end_index = len(view_revisions) - 1
1188
 
        view_revisions = view_revisions[start_index:end_index+1]
1189
 
    return view_revisions
1190
 
 
1191
 
 
1192
1165
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1193
1166
    include_merges=True):
1194
1167
    r"""Return the list of revision ids which touch a given file id.
1273
1246
    return result
1274
1247
 
1275
1248
 
1276
 
@deprecated_function(deprecated_in((2, 2, 0)))
1277
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1278
 
                       include_merges=True):
1279
 
    """Produce an iterator of revisions to show
1280
 
    :return: an iterator of (revision_id, revno, merge_depth)
1281
 
    (if there is no revno for a revision, None is supplied)
1282
 
    """
1283
 
    if not include_merges:
1284
 
        revision_ids = mainline_revs[1:]
1285
 
        if direction == 'reverse':
1286
 
            revision_ids.reverse()
1287
 
        for revision_id in revision_ids:
1288
 
            yield revision_id, str(rev_nos[revision_id]), 0
1289
 
        return
1290
 
    graph = branch.repository.get_graph()
1291
 
    # This asks for all mainline revisions, which means we only have to spider
1292
 
    # sideways, rather than depth history. That said, its still size-of-history
1293
 
    # and should be addressed.
1294
 
    # mainline_revisions always includes an extra revision at the beginning, so
1295
 
    # don't request it.
1296
 
    parent_map = dict(((key, value) for key, value in
1297
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1298
 
    # filter out ghosts; merge_sort errors on ghosts.
1299
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1300
 
    merge_sorted_revisions = tsort.merge_sort(
1301
 
        rev_graph,
1302
 
        mainline_revs[-1],
1303
 
        mainline_revs,
1304
 
        generate_revno=True)
1305
 
 
1306
 
    if direction == 'forward':
1307
 
        # forward means oldest first.
1308
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1309
 
    elif direction != 'reverse':
1310
 
        raise ValueError('invalid direction %r' % direction)
1311
 
 
1312
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1313
 
         ) in merge_sorted_revisions:
1314
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1315
 
 
1316
 
 
1317
1249
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1318
1250
    """Reverse revisions by depth.
1319
1251
 
1416
1348
        """Create a LogFormatter.
1417
1349
 
1418
1350
        :param to_file: the file to output to
1419
 
        :param to_exact_file: if set, gives an output stream to which 
 
1351
        :param to_exact_file: if set, gives an output stream to which
1420
1352
             non-Unicode diffs are written.
1421
1353
        :param show_ids: if True, revision-ids are to be displayed
1422
1354
        :param show_timezone: the timezone to use
1658
1590
        if revision.delta is not None:
1659
1591
            # Use the standard status output to display changes
1660
1592
            from bzrlib.delta import report_delta
1661
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1593
            report_delta(to_file, revision.delta, short_status=False,
1662
1594
                         show_ids=self.show_ids, indent=indent)
1663
1595
        if revision.diff is not None:
1664
1596
            to_file.write(indent + 'diff:\n')
1730
1662
        if revision.delta is not None:
1731
1663
            # Use the standard status output to display changes
1732
1664
            from bzrlib.delta import report_delta
1733
 
            report_delta(to_file, revision.delta, 
1734
 
                         short_status=self.delta_format==1, 
 
1665
            report_delta(to_file, revision.delta,
 
1666
                         short_status=self.delta_format==1,
1735
1667
                         show_ids=self.show_ids, indent=indent + offset)
1736
1668
        if revision.diff is not None:
1737
1669
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1917
1849
                              'The committer')
1918
1850
 
1919
1851
 
1920
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1921
 
    # deprecated; for compatibility
1922
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1923
 
    lf.show(revno, rev, delta)
1924
 
 
1925
 
 
1926
1852
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1927
1853
                           log_format='long'):
1928
1854
    """Show the change in revision history comparing the old revision history to the new one.