/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-07-15 14:13:32 UTC
  • mto: This revision was merged to the branch mainline in revision 6030.
  • Revision ID: v.ladeuil+lp@free.fr-20110715141332-ohkbf3u3xgzdmqq1
Remove trace.info, trace.log_error and trace.error deprecated in 2.1.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
73
73
    repository as _mod_repository,
74
74
    revision as _mod_revision,
75
75
    revisionspec,
76
 
    trace,
77
76
    tsort,
 
77
    i18n,
78
78
    )
79
79
""")
80
80
 
81
81
from bzrlib import (
 
82
    lazy_regex,
82
83
    registry,
83
84
    )
84
85
from bzrlib.osutils import (
85
86
    format_date,
86
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
87
89
    get_terminal_encoding,
88
 
    re_compile_checked,
89
90
    terminal_width,
90
91
    )
91
92
from bzrlib.symbol_versioning import (
110
111
    revno = 1
111
112
    for revision_id in branch.revision_history():
112
113
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if file_id in this_inv:
 
114
        if this_inv.has_id(file_id):
114
115
            this_ie = this_inv[file_id]
115
116
            this_path = this_inv.id2path(file_id)
116
117
        else:
156
157
             end_revision=None,
157
158
             search=None,
158
159
             limit=None,
159
 
             show_diff=False):
 
160
             show_diff=False,
 
161
             match=None):
160
162
    """Write out human-readable log of commits to this branch.
161
163
 
162
164
    This function is being retained for backwards compatibility but
185
187
        if None or 0.
186
188
 
187
189
    :param show_diff: If True, output a diff after each revision.
 
190
    
 
191
    :param match: Dictionary of search lists to use when matching revision
 
192
      properties.
188
193
    """
189
194
    # Convert old-style parameters to new-style parameters
190
195
    if specific_fileid is not None:
230
235
                          message_search=None, levels=1, generate_tags=True,
231
236
                          delta_type=None,
232
237
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
 
238
                          exclude_common_ancestry=False, match=None,
 
239
                          signature=False,
234
240
                          ):
235
241
    """Convenience function for making a logging request dictionary.
236
242
 
260
266
      generate; 1 for just the mainline; 0 for all levels.
261
267
 
262
268
    :param generate_tags: If True, include tags for matched revisions.
263
 
 
 
269
`
264
270
    :param delta_type: Either 'full', 'partial' or None.
265
271
      'full' means generate the complete delta - adds/deletes/modifies/etc;
266
272
      'partial' means filter the delta using specific_fileids;
278
284
 
279
285
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
286
      range operator or as a graph difference.
 
287
 
 
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
      
281
294
    """
 
295
    
 
296
    # Take care of old style message_search parameter
 
297
    if message_search:
 
298
        if match:
 
299
            if 'message' in match:
 
300
                match['message'].append(message_search)
 
301
            else:
 
302
                match['message'] = [message_search]
 
303
        else:
 
304
            match={ 'message': [message_search] }
 
305
        
282
306
    return {
283
307
        'direction': direction,
284
308
        'specific_fileids': specific_fileids,
285
309
        'start_revision': start_revision,
286
310
        'end_revision': end_revision,
287
311
        'limit': limit,
288
 
        'message_search': message_search,
289
312
        'levels': levels,
290
313
        'generate_tags': generate_tags,
291
314
        'delta_type': delta_type,
292
315
        'diff_type': diff_type,
293
316
        'exclude_common_ancestry': exclude_common_ancestry,
 
317
        'signature': signature,
 
318
        'match': match,
294
319
        # Add 'private' attributes for features that may be deprecated
295
320
        '_match_using_deltas': _match_using_deltas,
296
321
    }
298
323
 
299
324
def _apply_log_request_defaults(rqst):
300
325
    """Apply default values to a request dictionary."""
301
 
    result = _DEFAULT_REQUEST_PARAMS
 
326
    result = _DEFAULT_REQUEST_PARAMS.copy()
302
327
    if rqst:
303
328
        result.update(rqst)
304
329
    return result
305
330
 
306
331
 
 
332
def format_signature_validity(rev_id, repo):
 
333
    """get the signature validity
 
334
    
 
335
    :param rev_id: revision id to validate
 
336
    :param repo: repository of revision
 
337
    :return: human readable string to print to log
 
338
    """
 
339
    from bzrlib import gpg
 
340
 
 
341
    gpg_strategy = gpg.GPGStrategy(None)
 
342
    result = repo.verify_revision(rev_id, gpg_strategy)
 
343
    if result[0] == gpg.SIGNATURE_VALID:
 
344
        return "valid signature from {0}".format(result[1])
 
345
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
346
        return "unknown key {0}".format(result[1])
 
347
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
348
        return "invalid signature!"
 
349
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
350
        return "no signature"
 
351
 
 
352
 
307
353
class LogGenerator(object):
308
354
    """A generator of log revisions."""
309
355
 
361
407
            rqst['delta_type'] = None
362
408
        if not getattr(lf, 'supports_diff', False):
363
409
            rqst['diff_type'] = None
 
410
        if not getattr(lf, 'supports_signatures', False):
 
411
            rqst['signature'] = False
364
412
 
365
413
        # Find and print the interesting revisions
366
414
        generator = self._generator_factory(self.branch, rqst)
400
448
        levels = rqst.get('levels')
401
449
        limit = rqst.get('limit')
402
450
        diff_type = rqst.get('diff_type')
 
451
        show_signature = rqst.get('signature')
403
452
        log_count = 0
404
453
        revision_iterator = self._create_log_revision_iterator()
405
454
        for revs in revision_iterator:
411
460
                    diff = None
412
461
                else:
413
462
                    diff = self._format_diff(rev, rev_id, diff_type)
 
463
                if show_signature:
 
464
                    signature = format_signature_validity(rev_id,
 
465
                                                self.branch.repository)
 
466
                else:
 
467
                    signature = None
414
468
                yield LogRevision(rev, revno, merge_depth, delta,
415
 
                    self.rev_tag_dict.get(rev_id), diff)
 
469
                    self.rev_tag_dict.get(rev_id), diff, signature)
416
470
                if limit:
417
471
                    log_count += 1
418
472
                    if log_count >= limit:
432
486
        else:
433
487
            specific_files = None
434
488
        s = StringIO()
 
489
        path_encoding = get_diff_header_encoding()
435
490
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
436
 
            new_label='')
 
491
            new_label='', path_encoding=path_encoding)
437
492
        return s.getvalue()
438
493
 
439
494
    def _create_log_revision_iterator(self):
472
527
 
473
528
        # Apply the other filters
474
529
        return make_log_rev_iterator(self.branch, view_revisions,
475
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
530
            rqst.get('delta_type'), rqst.get('match'),
476
531
            file_ids=rqst.get('specific_fileids'),
477
532
            direction=rqst.get('direction'))
478
533
 
491
546
            rqst.get('specific_fileids')[0], view_revisions,
492
547
            include_merges=rqst.get('levels') != 1)
493
548
        return make_log_rev_iterator(self.branch, view_revisions,
494
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
549
            rqst.get('delta_type'), rqst.get('match'))
495
550
 
496
551
 
497
552
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
522
577
    elif not generate_merge_revisions:
523
578
        # If we only want to see linear revisions, we can iterate ...
524
579
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
525
 
                                             direction)
 
580
                                             direction, exclude_common_ancestry)
526
581
        if direction == 'forward':
527
582
            iter_revs = reversed(iter_revs)
528
583
    else:
539
594
        # It's the tip
540
595
        return [(br_rev_id, br_revno, 0)]
541
596
    else:
542
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
543
 
        revno_str = '.'.join(str(n) for n in revno)
 
597
        revno_str = _compute_revno_str(branch, rev_id)
544
598
        return [(rev_id, revno_str, 0)]
545
599
 
546
600
 
547
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
548
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
 
601
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
 
602
                             exclude_common_ancestry=False):
 
603
    result = _linear_view_revisions(
 
604
        branch, start_rev_id, end_rev_id,
 
605
        exclude_common_ancestry=exclude_common_ancestry)
549
606
    # If a start limit was given and it's not obviously an
550
607
    # ancestor of the end limit, check it before outputting anything
551
608
    if direction == 'forward' or (start_rev_id
572
629
    if delayed_graph_generation:
573
630
        try:
574
631
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
632
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
633
                if _has_merges(branch, rev_id):
577
634
                    # The end_rev_id can be nested down somewhere. We need an
578
635
                    # explicit ancestry check. There is an ambiguity here as we
623
680
    return len(parents) > 1
624
681
 
625
682
 
 
683
def _compute_revno_str(branch, rev_id):
 
684
    """Compute the revno string from a rev_id.
 
685
 
 
686
    :return: The revno string, or None if the revision is not in the supplied
 
687
        branch.
 
688
    """
 
689
    try:
 
690
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
691
    except errors.NoSuchRevision:
 
692
        # The revision must be outside of this branch
 
693
        return None
 
694
    else:
 
695
        return '.'.join(str(n) for n in revno)
 
696
 
 
697
 
626
698
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
699
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
628
700
    if start_rev_id and end_rev_id:
629
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
630
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
701
        try:
 
702
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
703
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
704
        except errors.NoSuchRevision:
 
705
            # one or both is not in the branch; not obvious
 
706
            return False
631
707
        if len(start_dotted) == 1 and len(end_dotted) == 1:
632
708
            # both on mainline
633
709
            return start_dotted[0] <= end_dotted[0]
643
719
    return True
644
720
 
645
721
 
646
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
722
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
723
                           exclude_common_ancestry=False):
647
724
    """Calculate a sequence of revisions to view, newest to oldest.
648
725
 
649
726
    :param start_rev_id: the lower revision-id
650
727
    :param end_rev_id: the upper revision-id
 
728
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
729
        the iterated revisions.
651
730
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
652
731
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
 
      is not found walking the left-hand history
 
732
        is not found walking the left-hand history
654
733
    """
655
734
    br_revno, br_rev_id = branch.last_revision_info()
656
735
    repo = branch.repository
 
736
    graph = repo.get_graph()
657
737
    if start_rev_id is None and end_rev_id is None:
658
738
        cur_revno = br_revno
659
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
739
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
740
            (_mod_revision.NULL_REVISION,)):
660
741
            yield revision_id, str(cur_revno), 0
661
742
            cur_revno -= 1
662
743
    else:
663
744
        if end_rev_id is None:
664
745
            end_rev_id = br_rev_id
665
746
        found_start = start_rev_id is None
666
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
667
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
668
 
            revno_str = '.'.join(str(n) for n in revno)
 
747
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
748
                (_mod_revision.NULL_REVISION,)):
 
749
            revno_str = _compute_revno_str(branch, revision_id)
669
750
            if not found_start and revision_id == start_rev_id:
670
 
                yield revision_id, revno_str, 0
 
751
                if not exclude_common_ancestry:
 
752
                    yield revision_id, revno_str, 0
671
753
                found_start = True
672
754
                break
673
755
            else:
789
871
    return log_rev_iterator
790
872
 
791
873
 
792
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
874
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
793
875
    """Create a filtered iterator of log_rev_iterator matching on a regex.
794
876
 
795
877
    :param branch: The branch being logged.
796
878
    :param generate_delta: Whether to generate a delta for each revision.
797
 
    :param search: A user text search string.
 
879
    :param match: A dictionary with properties as keys and lists of strings
 
880
        as values. To match, a revision may match any of the supplied strings
 
881
        within a single property but must match at least one string for each
 
882
        property.
798
883
    :param log_rev_iterator: An input iterator containing all revisions that
799
884
        could be displayed, in lists.
800
885
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
801
886
        delta).
802
887
    """
803
 
    if search is None:
 
888
    if match is None:
804
889
        return log_rev_iterator
805
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
806
 
            'log message filter')
807
 
    return _filter_message_re(searchRE, log_rev_iterator)
808
 
 
809
 
 
810
 
def _filter_message_re(searchRE, log_rev_iterator):
 
890
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v]) 
 
891
                for (k,v) in match.iteritems()]
 
892
    return _filter_re(searchRE, log_rev_iterator)
 
893
 
 
894
 
 
895
def _filter_re(searchRE, log_rev_iterator):
811
896
    for revs in log_rev_iterator:
812
 
        new_revs = []
813
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
814
 
            if searchRE.search(rev.message):
815
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
816
 
        yield new_revs
817
 
 
 
897
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
898
        if new_revs:
 
899
            yield new_revs
 
900
 
 
901
def _match_filter(searchRE, rev):
 
902
    strings = {
 
903
               'message': (rev.message,),
 
904
               'committer': (rev.committer,),
 
905
               'author': (rev.get_apparent_authors()),
 
906
               'bugs': list(rev.iter_bugs())
 
907
               }
 
908
    strings[''] = [item for inner_list in strings.itervalues() 
 
909
                   for item in inner_list]
 
910
    
 
911
    for (k,v) in searchRE:
 
912
        if k in strings and not _match_any_filter(strings[k], v):
 
913
            return False
 
914
    return True
 
915
 
 
916
def _match_any_filter(strings, res):
 
917
    return any([filter(None, map(re.search, strings)) for re in res])
818
918
 
819
919
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
820
920
    fileids=None, direction='reverse'):
940
1040
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
941
1041
        delta).
942
1042
    """
943
 
    repository = branch.repository
944
1043
    num = 9
945
1044
    for batch in log_rev_iterator:
946
1045
        batch = iter(batch)
1063
1162
    cur_revno = branch_revno
1064
1163
    rev_nos = {}
1065
1164
    mainline_revs = []
1066
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1067
 
                        branch_last_revision):
 
1165
    graph = branch.repository.get_graph()
 
1166
    for revision_id in graph.iter_lefthand_ancestry(
 
1167
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1168
        if cur_revno < start_revno:
1069
1169
            # We have gone far enough, but we always add 1 more revision
1070
1170
            rev_nos[revision_id] = cur_revno
1136
1236
    This includes the revisions which directly change the file id,
1137
1237
    and the revisions which merge these changes. So if the
1138
1238
    revision graph is::
 
1239
 
1139
1240
        A-.
1140
1241
        |\ \
1141
1242
        B C E
1168
1269
    """
1169
1270
    # Lookup all possible text keys to determine which ones actually modified
1170
1271
    # the file.
 
1272
    graph = branch.repository.get_file_graph()
 
1273
    get_parent_map = graph.get_parent_map
1171
1274
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1275
    next_keys = None
1173
1276
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1280
    #       indexing layer. We might consider passing in hints as to the known
1178
1281
    #       access pattern (sparse/clustered, high success rate/low success
1179
1282
    #       rate). This particular access is clustered with a low success rate.
1180
 
    get_parent_map = branch.repository.texts.get_parent_map
1181
1283
    modified_text_revisions = set()
1182
1284
    chunk_size = 1000
1183
1285
    for start in xrange(0, len(text_keys), chunk_size):
1291
1393
    """
1292
1394
 
1293
1395
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
 
                 tags=None, diff=None):
 
1396
                 tags=None, diff=None, signature=None):
1295
1397
        self.rev = rev
1296
 
        self.revno = str(revno)
 
1398
        if revno is None:
 
1399
            self.revno = None
 
1400
        else:
 
1401
            self.revno = str(revno)
1297
1402
        self.merge_depth = merge_depth
1298
1403
        self.delta = delta
1299
1404
        self.tags = tags
1300
1405
        self.diff = diff
 
1406
        self.signature = signature
1301
1407
 
1302
1408
 
1303
1409
class LogFormatter(object):
1312
1418
    to indicate which LogRevision attributes it supports:
1313
1419
 
1314
1420
    - supports_delta must be True if this log formatter supports delta.
1315
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1316
 
        attribute describes whether the 'short_status' format (1) or the long
1317
 
        one (2) should be used.
 
1421
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1422
      attribute describes whether the 'short_status' format (1) or the long
 
1423
      one (2) should be used.
1318
1424
 
1319
1425
    - supports_merge_revisions must be True if this log formatter supports
1320
 
        merge revisions.  If not, then only mainline revisions will be passed
1321
 
        to the formatter.
 
1426
      merge revisions.  If not, then only mainline revisions will be passed
 
1427
      to the formatter.
1322
1428
 
1323
1429
    - preferred_levels is the number of levels this formatter defaults to.
1324
 
        The default value is zero meaning display all levels.
1325
 
        This value is only relevant if supports_merge_revisions is True.
 
1430
      The default value is zero meaning display all levels.
 
1431
      This value is only relevant if supports_merge_revisions is True.
1326
1432
 
1327
1433
    - supports_tags must be True if this log formatter supports tags.
1328
 
        Otherwise the tags attribute may not be populated.
 
1434
      Otherwise the tags attribute may not be populated.
1329
1435
 
1330
1436
    - supports_diff must be True if this log formatter supports diffs.
1331
 
        Otherwise the diff attribute may not be populated.
 
1437
      Otherwise the diff attribute may not be populated.
 
1438
 
 
1439
    - supports_signatures must be True if this log formatter supports GPG
 
1440
      signatures.
1332
1441
 
1333
1442
    Plugins can register functions to show custom revision properties using
1334
1443
    the properties_handler_registry. The registered function
1335
 
    must respect the following interface description:
 
1444
    must respect the following interface description::
 
1445
 
1336
1446
        def my_show_properties(properties_dict):
1337
1447
            # code that returns a dict {'name':'value'} of the properties
1338
1448
            # to be shown
1341
1451
 
1342
1452
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1453
                 delta_format=None, levels=None, show_advice=False,
1344
 
                 to_exact_file=None):
 
1454
                 to_exact_file=None, author_list_handler=None):
1345
1455
        """Create a LogFormatter.
1346
1456
 
1347
1457
        :param to_file: the file to output to
1355
1465
          let the log formatter decide.
1356
1466
        :param show_advice: whether to show advice at the end of the
1357
1467
          log or not
 
1468
        :param author_list_handler: callable generating a list of
 
1469
          authors to display for a given revision
1358
1470
        """
1359
1471
        self.to_file = to_file
1360
1472
        # 'exact' stream used to show diff, it should print content 'as is'
1375
1487
        self.levels = levels
1376
1488
        self._show_advice = show_advice
1377
1489
        self._merge_count = 0
 
1490
        self._author_list_handler = author_list_handler
1378
1491
 
1379
1492
    def get_levels(self):
1380
1493
        """Get the number of levels to display or 0 for all."""
1412
1525
        return address
1413
1526
 
1414
1527
    def short_author(self, rev):
1415
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1416
 
        if name:
1417
 
            return name
1418
 
        return address
 
1528
        return self.authors(rev, 'first', short=True, sep=', ')
 
1529
 
 
1530
    def authors(self, rev, who, short=False, sep=None):
 
1531
        """Generate list of authors, taking --authors option into account.
 
1532
 
 
1533
        The caller has to specify the name of a author list handler,
 
1534
        as provided by the author list registry, using the ``who``
 
1535
        argument.  That name only sets a default, though: when the
 
1536
        user selected a different author list generation using the
 
1537
        ``--authors`` command line switch, as represented by the
 
1538
        ``author_list_handler`` constructor argument, that value takes
 
1539
        precedence.
 
1540
 
 
1541
        :param rev: The revision for which to generate the list of authors.
 
1542
        :param who: Name of the default handler.
 
1543
        :param short: Whether to shorten names to either name or address.
 
1544
        :param sep: What separator to use for automatic concatenation.
 
1545
        """
 
1546
        if self._author_list_handler is not None:
 
1547
            # The user did specify --authors, which overrides the default
 
1548
            author_list_handler = self._author_list_handler
 
1549
        else:
 
1550
            # The user didn't specify --authors, so we use the caller's default
 
1551
            author_list_handler = author_list_registry.get(who)
 
1552
        names = author_list_handler(rev)
 
1553
        if short:
 
1554
            for i in range(len(names)):
 
1555
                name, address = config.parse_username(names[i])
 
1556
                if name:
 
1557
                    names[i] = name
 
1558
                else:
 
1559
                    names[i] = address
 
1560
        if sep is not None:
 
1561
            names = sep.join(names)
 
1562
        return names
1419
1563
 
1420
1564
    def merge_marker(self, revision):
1421
1565
        """Get the merge marker to include in the output or '' if none."""
1491
1635
    supports_delta = True
1492
1636
    supports_tags = True
1493
1637
    supports_diff = True
 
1638
    supports_signatures = True
1494
1639
 
1495
1640
    def __init__(self, *args, **kwargs):
1496
1641
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1516
1661
                self.merge_marker(revision)))
1517
1662
        if revision.tags:
1518
1663
            lines.append('tags: %s' % (', '.join(revision.tags)))
1519
 
        if self.show_ids:
 
1664
        if self.show_ids or revision.revno is None:
1520
1665
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1666
        if self.show_ids:
1521
1667
            for parent_id in revision.rev.parent_ids:
1522
1668
                lines.append('parent: %s' % (parent_id,))
1523
1669
        lines.extend(self.custom_properties(revision.rev))
1524
1670
 
1525
1671
        committer = revision.rev.committer
1526
 
        authors = revision.rev.get_apparent_authors()
 
1672
        authors = self.authors(revision.rev, 'all')
1527
1673
        if authors != [committer]:
1528
1674
            lines.append('author: %s' % (", ".join(authors),))
1529
1675
        lines.append('committer: %s' % (committer,))
1534
1680
 
1535
1681
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1536
1682
 
 
1683
        if revision.signature is not None:
 
1684
            lines.append('signature: ' + revision.signature)
 
1685
 
1537
1686
        lines.append('message:')
1538
1687
        if not revision.rev.message:
1539
1688
            lines.append('  (no message)')
1586
1735
        indent = '    ' * depth
1587
1736
        revno_width = self.revno_width_by_depth.get(depth)
1588
1737
        if revno_width is None:
1589
 
            if revision.revno.find('.') == -1:
 
1738
            if revision.revno is None or revision.revno.find('.') == -1:
1590
1739
                # mainline revno, e.g. 12345
1591
1740
                revno_width = 5
1592
1741
            else:
1600
1749
        if revision.tags:
1601
1750
            tags = ' {%s}' % (', '.join(revision.tags))
1602
1751
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
 
                revision.revno, self.short_author(revision.rev),
 
1752
                revision.revno or "", self.short_author(revision.rev),
1604
1753
                format_date(revision.rev.timestamp,
1605
1754
                            revision.rev.timezone or 0,
1606
1755
                            self.show_timezone, date_fmt="%Y-%m-%d",
1607
1756
                            show_offset=False),
1608
1757
                tags, self.merge_marker(revision)))
1609
1758
        self.show_properties(revision.rev, indent+offset)
1610
 
        if self.show_ids:
 
1759
        if self.show_ids or revision.revno is None:
1611
1760
            to_file.write(indent + offset + 'revision-id:%s\n'
1612
1761
                          % (revision.rev.revision_id,))
1613
1762
        if not revision.rev.message:
1666
1815
 
1667
1816
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1817
        """Format log info into one string. Truncate tail of string
1669
 
        :param  revno:      revision number or None.
1670
 
                            Revision numbers counts from 1.
1671
 
        :param  rev:        revision object
1672
 
        :param  max_chars:  maximum length of resulting string
1673
 
        :param  tags:       list of tags or None
1674
 
        :param  prefix:     string to prefix each line
1675
 
        :return:            formatted truncated string
 
1818
 
 
1819
        :param revno:      revision number or None.
 
1820
                           Revision numbers counts from 1.
 
1821
        :param rev:        revision object
 
1822
        :param max_chars:  maximum length of resulting string
 
1823
        :param tags:       list of tags or None
 
1824
        :param prefix:     string to prefix each line
 
1825
        :return:           formatted truncated string
1676
1826
        """
1677
1827
        out = []
1678
1828
        if revno:
1679
1829
            # show revno only when is not None
1680
1830
            out.append("%s:" % revno)
1681
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1831
        if max_chars is not None:
 
1832
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1833
        else:
 
1834
            out.append(self.short_author(rev))
1682
1835
        out.append(self.date_string(rev))
1683
1836
        if len(rev.parent_ids) > 1:
1684
1837
            out.append('[merge]')
1703
1856
                               self.show_timezone,
1704
1857
                               date_fmt='%Y-%m-%d',
1705
1858
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
 
1859
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1860
        committer_str = committer_str.replace(' <', '  <')
1707
1861
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1708
1862
 
1709
1863
        if revision.delta is not None and revision.delta.has_changed():
1774
1928
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1775
1929
 
1776
1930
 
 
1931
def author_list_all(rev):
 
1932
    return rev.get_apparent_authors()[:]
 
1933
 
 
1934
 
 
1935
def author_list_first(rev):
 
1936
    lst = rev.get_apparent_authors()
 
1937
    try:
 
1938
        return [lst[0]]
 
1939
    except IndexError:
 
1940
        return []
 
1941
 
 
1942
 
 
1943
def author_list_committer(rev):
 
1944
    return [rev.committer]
 
1945
 
 
1946
 
 
1947
author_list_registry = registry.Registry()
 
1948
 
 
1949
author_list_registry.register('all', author_list_all,
 
1950
                              'All authors')
 
1951
 
 
1952
author_list_registry.register('first', author_list_first,
 
1953
                              'The first author')
 
1954
 
 
1955
author_list_registry.register('committer', author_list_committer,
 
1956
                              'The committer')
 
1957
 
 
1958
 
1777
1959
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
1960
    # deprecated; for compatibility
1779
1961
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1848
2030
    old_revisions = set()
1849
2031
    new_history = []
1850
2032
    new_revisions = set()
1851
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1852
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
2033
    graph = repository.get_graph()
 
2034
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
2035
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1853
2036
    stop_revision = None
1854
2037
    do_old = True
1855
2038
    do_new = True
1930
2113
        lf.log_revision(lr)
1931
2114
 
1932
2115
 
1933
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2116
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2117
    """Find file-ids and kinds given a list of files and a revision range.
1935
2118
 
1936
2119
    We search for files at the end of the range. If not found there,
1940
2123
    :param file_list: the list of paths given on the command line;
1941
2124
      the first of these can be a branch location or a file path,
1942
2125
      the remainder must be file paths
 
2126
    :param add_cleanup: When the branch returned is read locked,
 
2127
      an unlock call will be queued to the cleanup.
1943
2128
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2129
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
2130
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2131
      branch will be read-locked.
1947
2132
    """
1948
 
    from builtins import _get_revision_range, safe_relpath_files
 
2133
    from builtins import _get_revision_range
1949
2134
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
 
2135
    add_cleanup(b.lock_read().unlock)
1951
2136
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
2137
    # those paths might be deleted ones, they might be on a case-insensitive
1953
2138
    # filesystem and/or they might be in silly locations (like another branch).
1957
2142
    # case of running log in a nested directory, assuming paths beyond the
1958
2143
    # first one haven't been deleted ...
1959
2144
    if tree:
1960
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2145
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1961
2146
    else:
1962
2147
        relpaths = [path] + file_list[1:]
1963
2148
    info_list = []