~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jonathan Riddell
  • Date: 2011-06-24 13:57:59 UTC
  • mto: This revision was merged to the branch mainline in revision 6003.
  • Revision ID: jriddell@canonical.com-20110624135759-263hx2et2kl1in39
change directory option to location argument

Show diffs side-by-side

added added

removed removed

Lines of Context:
79
79
""")
80
80
 
81
81
from bzrlib import (
82
 
    lazy_regex,
83
82
    registry,
84
83
    )
85
84
from bzrlib.osutils import (
89
88
    get_terminal_encoding,
90
89
    terminal_width,
91
90
    )
 
91
from bzrlib.symbol_versioning import (
 
92
    deprecated_function,
 
93
    deprecated_in,
 
94
    )
92
95
 
93
96
 
94
97
def find_touching_revisions(branch, file_id):
107
110
    revno = 1
108
111
    for revision_id in branch.revision_history():
109
112
        this_inv = branch.repository.get_inventory(revision_id)
110
 
        if this_inv.has_id(file_id):
 
113
        if file_id in this_inv:
111
114
            this_ie = this_inv[file_id]
112
115
            this_path = this_inv.id2path(file_id)
113
116
        else:
153
156
             end_revision=None,
154
157
             search=None,
155
158
             limit=None,
156
 
             show_diff=False,
157
 
             match=None):
 
159
             show_diff=False):
158
160
    """Write out human-readable log of commits to this branch.
159
161
 
160
162
    This function is being retained for backwards compatibility but
183
185
        if None or 0.
184
186
 
185
187
    :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
188
    """
190
189
    # Convert old-style parameters to new-style parameters
191
190
    if specific_fileid is not None:
219
218
# make_log_request_dict() below
220
219
_DEFAULT_REQUEST_PARAMS = {
221
220
    'direction': 'reverse',
222
 
    'levels': None,
 
221
    'levels': 1,
223
222
    'generate_tags': True,
224
223
    'exclude_common_ancestry': False,
225
224
    '_match_using_deltas': True,
228
227
 
229
228
def make_log_request_dict(direction='reverse', specific_fileids=None,
230
229
                          start_revision=None, end_revision=None, limit=None,
231
 
                          message_search=None, levels=None, generate_tags=True,
 
230
                          message_search=None, levels=1, generate_tags=True,
232
231
                          delta_type=None,
233
232
                          diff_type=None, _match_using_deltas=True,
234
 
                          exclude_common_ancestry=False, match=None,
 
233
                          exclude_common_ancestry=False,
235
234
                          signature=False,
236
235
                          ):
237
236
    """Convenience function for making a logging request dictionary.
259
258
      matching commit messages
260
259
 
261
260
    :param levels: the number of levels of revisions to
262
 
      generate; 1 for just the mainline; 0 for all levels, or None for
263
 
      a sensible default.
 
261
      generate; 1 for just the mainline; 0 for all levels.
264
262
 
265
263
    :param generate_tags: If True, include tags for matched revisions.
266
264
`
283
281
      range operator or as a graph difference.
284
282
 
285
283
    :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
284
    """
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] }
301
285
    return {
302
286
        'direction': direction,
303
287
        'specific_fileids': specific_fileids,
304
288
        'start_revision': start_revision,
305
289
        'end_revision': end_revision,
306
290
        'limit': limit,
 
291
        'message_search': message_search,
307
292
        'levels': levels,
308
293
        'generate_tags': generate_tags,
309
294
        'delta_type': delta_type,
310
295
        'diff_type': diff_type,
311
296
        'exclude_common_ancestry': exclude_common_ancestry,
312
297
        'signature': signature,
313
 
        'match': match,
314
298
        # Add 'private' attributes for features that may be deprecated
315
299
        '_match_using_deltas': _match_using_deltas,
316
300
    }
326
310
 
327
311
def format_signature_validity(rev_id, repo):
328
312
    """get the signature validity
329
 
 
 
313
    
330
314
    :param rev_id: revision id to validate
331
315
    :param repo: repository of revision
332
316
    :return: human readable string to print to log
336
320
    gpg_strategy = gpg.GPGStrategy(None)
337
321
    result = repo.verify_revision(rev_id, gpg_strategy)
338
322
    if result[0] == gpg.SIGNATURE_VALID:
339
 
        return "valid signature from {0}".format(result[1])
 
323
        return i18n.gettext("valid signature from {0}").format(result[1])
340
324
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
341
 
        return "unknown key {0}".format(result[1])
 
325
        return i18n.gettext("unknown key {0}").format(result[1])
342
326
    if result[0] == gpg.SIGNATURE_NOT_VALID:
343
 
        return "invalid signature!"
 
327
        return i18n.gettext("invalid signature!")
344
328
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
345
 
        return "no signature"
 
329
        return i18n.gettext("no signature")
346
330
 
347
331
 
348
332
class LogGenerator(object):
395
379
        # Tweak the LogRequest based on what the LogFormatter can handle.
396
380
        # (There's no point generating stuff if the formatter can't display it.)
397
381
        rqst = self.rqst
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
 
 
 
382
        rqst['levels'] = lf.get_levels()
402
383
        if not getattr(lf, 'supports_tags', False):
403
384
            rqst['generate_tags'] = False
404
385
        if not getattr(lf, 'supports_delta', False):
416
397
 
417
398
    def _generator_factory(self, branch, rqst):
418
399
        """Make the LogGenerator object to use.
419
 
 
 
400
        
420
401
        Subclasses may wish to override this.
421
402
        """
422
403
        return _DefaultLogGenerator(branch, rqst)
525
506
 
526
507
        # Apply the other filters
527
508
        return make_log_rev_iterator(self.branch, view_revisions,
528
 
            rqst.get('delta_type'), rqst.get('match'),
 
509
            rqst.get('delta_type'), rqst.get('message_search'),
529
510
            file_ids=rqst.get('specific_fileids'),
530
511
            direction=rqst.get('direction'))
531
512
 
544
525
            rqst.get('specific_fileids')[0], view_revisions,
545
526
            include_merges=rqst.get('levels') != 1)
546
527
        return make_log_rev_iterator(self.branch, view_revisions,
547
 
            rqst.get('delta_type'), rqst.get('match'))
 
528
            rqst.get('delta_type'), rqst.get('message_search'))
548
529
 
549
530
 
550
531
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
801
782
            yield rev_id, '.'.join(map(str, revno)), merge_depth
802
783
 
803
784
 
 
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, match, log_rev_iterator):
 
853
def _make_search_filter(branch, generate_delta, search, 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 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.
 
858
    :param search: A user text search string.
862
859
    :param log_rev_iterator: An input iterator containing all revisions that
863
860
        could be displayed, in lists.
864
861
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
865
862
        delta).
866
863
    """
867
 
    if match is None:
 
864
    if search is None:
868
865
        return 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):
 
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):
875
871
    for revs in log_rev_iterator:
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])
 
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
 
896
878
 
897
879
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
898
880
    fileids=None, direction='reverse'):
971
953
 
972
954
def _update_fileids(delta, fileids, stop_on):
973
955
    """Update the set of file-ids to search based on file lifecycle events.
974
 
 
 
956
    
975
957
    :param fileids: a set of fileids to update
976
958
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
977
959
      fileids set once their add or remove entry is detected respectively
1018
1000
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1019
1001
        delta).
1020
1002
    """
 
1003
    repository = branch.repository
1021
1004
    num = 9
1022
1005
    for batch in log_rev_iterator:
1023
1006
        batch = iter(batch)
1162
1145
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1163
1146
 
1164
1147
 
 
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
 
1165
1192
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1166
1193
    include_merges=True):
1167
1194
    r"""Return the list of revision ids which touch a given file id.
1246
1273
    return result
1247
1274
 
1248
1275
 
 
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
 
1249
1317
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1250
1318
    """Reverse revisions by depth.
1251
1319
 
1348
1416
        """Create a LogFormatter.
1349
1417
 
1350
1418
        :param to_file: the file to output to
1351
 
        :param to_exact_file: if set, gives an output stream to which
 
1419
        :param to_exact_file: if set, gives an output stream to which 
1352
1420
             non-Unicode diffs are written.
1353
1421
        :param show_ids: if True, revision-ids are to be displayed
1354
1422
        :param show_timezone: the timezone to use
1590
1658
        if revision.delta is not None:
1591
1659
            # Use the standard status output to display changes
1592
1660
            from bzrlib.delta import report_delta
1593
 
            report_delta(to_file, revision.delta, short_status=False,
 
1661
            report_delta(to_file, revision.delta, short_status=False, 
1594
1662
                         show_ids=self.show_ids, indent=indent)
1595
1663
        if revision.diff is not None:
1596
1664
            to_file.write(indent + 'diff:\n')
1662
1730
        if revision.delta is not None:
1663
1731
            # Use the standard status output to display changes
1664
1732
            from bzrlib.delta import report_delta
1665
 
            report_delta(to_file, revision.delta,
1666
 
                         short_status=self.delta_format==1,
 
1733
            report_delta(to_file, revision.delta, 
 
1734
                         short_status=self.delta_format==1, 
1667
1735
                         show_ids=self.show_ids, indent=indent + offset)
1668
1736
        if revision.diff is not None:
1669
1737
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1849
1917
                              'The committer')
1850
1918
 
1851
1919
 
 
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
 
1852
1926
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1853
1927
                           log_format='long'):
1854
1928
    """Show the change in revision history comparing the old revision history to the new one.