~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

Add a NEWS entry and prepare submission.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2009 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,
76
77
    tsort,
77
78
    )
78
 
from bzrlib.i18n import gettext, ngettext
79
79
""")
80
80
 
81
81
from bzrlib import (
82
 
    lazy_regex,
83
82
    registry,
84
83
    )
85
84
from bzrlib.osutils import (
86
85
    format_date,
87
86
    format_date_with_offset_in_original_timezone,
88
 
    get_diff_header_encoding,
89
87
    get_terminal_encoding,
 
88
    re_compile_checked,
90
89
    terminal_width,
91
90
    )
92
91
 
106
105
    last_path = None
107
106
    revno = 1
108
107
    for revision_id in branch.revision_history():
109
 
        this_inv = branch.repository.get_inventory(revision_id)
110
 
        if this_inv.has_id(file_id):
 
108
        this_inv = branch.repository.get_revision_inventory(revision_id)
 
109
        if file_id in this_inv:
111
110
            this_ie = this_inv[file_id]
112
111
            this_path = this_inv.id2path(file_id)
113
112
        else:
153
152
             end_revision=None,
154
153
             search=None,
155
154
             limit=None,
156
 
             show_diff=False,
157
 
             match=None):
 
155
             show_diff=False):
158
156
    """Write out human-readable log of commits to this branch.
159
157
 
160
158
    This function is being retained for backwards compatibility but
183
181
        if None or 0.
184
182
 
185
183
    :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
184
    """
190
185
    # Convert old-style parameters to new-style parameters
191
186
    if specific_fileid is not None:
215
210
    Logger(branch, rqst).show(lf)
216
211
 
217
212
 
218
 
# Note: This needs to be kept in sync with the defaults in
 
213
# Note: This needs to be kept this in sync with the defaults in
219
214
# make_log_request_dict() below
220
215
_DEFAULT_REQUEST_PARAMS = {
221
216
    'direction': 'reverse',
222
 
    'levels': None,
 
217
    'levels': 1,
223
218
    'generate_tags': True,
224
 
    'exclude_common_ancestry': False,
225
219
    '_match_using_deltas': True,
226
220
    }
227
221
 
228
222
 
229
223
def make_log_request_dict(direction='reverse', specific_fileids=None,
230
 
                          start_revision=None, end_revision=None, limit=None,
231
 
                          message_search=None, levels=None, generate_tags=True,
232
 
                          delta_type=None,
233
 
                          diff_type=None, _match_using_deltas=True,
234
 
                          exclude_common_ancestry=False, match=None,
235
 
                          signature=False, omit_merges=False,
236
 
                          ):
 
224
    start_revision=None, end_revision=None, limit=None,
 
225
    message_search=None, levels=1, generate_tags=True, delta_type=None,
 
226
    diff_type=None, _match_using_deltas=True):
237
227
    """Convenience function for making a logging request dictionary.
238
228
 
239
229
    Using this function may make code slightly safer by ensuring
259
249
      matching commit messages
260
250
 
261
251
    :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.
 
252
      generate; 1 for just the mainline; 0 for all levels.
264
253
 
265
254
    :param generate_tags: If True, include tags for matched revisions.
266
 
`
 
255
 
267
256
    :param delta_type: Either 'full', 'partial' or None.
268
257
      'full' means generate the complete delta - adds/deletes/modifies/etc;
269
258
      'partial' means filter the delta using specific_fileids;
278
267
      algorithm used for matching specific_fileids. This parameter
279
268
      may be removed in the future so bzrlib client code should NOT
280
269
      use it.
281
 
 
282
 
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
283
 
      range operator or as a graph difference.
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
 
 
294
270
    """
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] }
304
271
    return {
305
272
        'direction': direction,
306
273
        'specific_fileids': specific_fileids,
307
274
        'start_revision': start_revision,
308
275
        'end_revision': end_revision,
309
276
        'limit': limit,
 
277
        'message_search': message_search,
310
278
        'levels': levels,
311
279
        'generate_tags': generate_tags,
312
280
        'delta_type': delta_type,
313
281
        'diff_type': diff_type,
314
 
        'exclude_common_ancestry': exclude_common_ancestry,
315
 
        'signature': signature,
316
 
        'match': match,
317
 
        'omit_merges': omit_merges,
318
282
        # Add 'private' attributes for features that may be deprecated
319
283
        '_match_using_deltas': _match_using_deltas,
320
284
    }
322
286
 
323
287
def _apply_log_request_defaults(rqst):
324
288
    """Apply default values to a request dictionary."""
325
 
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
289
    result = _DEFAULT_REQUEST_PARAMS
326
290
    if rqst:
327
291
        result.update(rqst)
328
292
    return result
329
293
 
330
294
 
331
 
def format_signature_validity(rev_id, repo):
332
 
    """get the signature validity
333
 
 
334
 
    :param rev_id: revision id to validate
335
 
    :param repo: repository of revision
336
 
    :return: human readable string to print to log
337
 
    """
338
 
    from bzrlib import gpg
339
 
 
340
 
    gpg_strategy = gpg.GPGStrategy(None)
341
 
    result = repo.verify_revision(rev_id, gpg_strategy)
342
 
    if result[0] == gpg.SIGNATURE_VALID:
343
 
        return "valid signature from {0}".format(result[1])
344
 
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
345
 
        return "unknown key {0}".format(result[1])
346
 
    if result[0] == gpg.SIGNATURE_NOT_VALID:
347
 
        return "invalid signature!"
348
 
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
349
 
        return "no signature"
350
 
 
351
 
 
352
295
class LogGenerator(object):
353
296
    """A generator of log revisions."""
354
297
 
361
304
 
362
305
 
363
306
class Logger(object):
364
 
    """An object that generates, formats and displays a log."""
 
307
    """An object the generates, formats and displays a log."""
365
308
 
366
309
    def __init__(self, branch, rqst):
367
310
        """Create a Logger.
399
342
        # Tweak the LogRequest based on what the LogFormatter can handle.
400
343
        # (There's no point generating stuff if the formatter can't display it.)
401
344
        rqst = self.rqst
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
 
 
 
345
        rqst['levels'] = lf.get_levels()
406
346
        if not getattr(lf, 'supports_tags', False):
407
347
            rqst['generate_tags'] = False
408
348
        if not getattr(lf, 'supports_delta', False):
409
349
            rqst['delta_type'] = None
410
350
        if not getattr(lf, 'supports_diff', False):
411
351
            rqst['diff_type'] = None
412
 
        if not getattr(lf, 'supports_signatures', False):
413
 
            rqst['signature'] = False
414
352
 
415
353
        # Find and print the interesting revisions
416
354
        generator = self._generator_factory(self.branch, rqst)
420
358
 
421
359
    def _generator_factory(self, branch, rqst):
422
360
        """Make the LogGenerator object to use.
423
 
 
 
361
        
424
362
        Subclasses may wish to override this.
425
363
        """
426
364
        return _DefaultLogGenerator(branch, rqst)
450
388
        levels = rqst.get('levels')
451
389
        limit = rqst.get('limit')
452
390
        diff_type = rqst.get('diff_type')
453
 
        show_signature = rqst.get('signature')
454
 
        omit_merges = rqst.get('omit_merges')
455
391
        log_count = 0
456
392
        revision_iterator = self._create_log_revision_iterator()
457
393
        for revs in revision_iterator:
459
395
                # 0 levels means show everything; merge_depth counts from 0
460
396
                if levels != 0 and merge_depth >= levels:
461
397
                    continue
462
 
                if omit_merges and len(rev.parent_ids) > 1:
463
 
                    continue
464
398
                if diff_type is None:
465
399
                    diff = None
466
400
                else:
467
401
                    diff = self._format_diff(rev, rev_id, diff_type)
468
 
                if show_signature:
469
 
                    signature = format_signature_validity(rev_id,
470
 
                                                self.branch.repository)
471
 
                else:
472
 
                    signature = None
473
402
                yield LogRevision(rev, revno, merge_depth, delta,
474
 
                    self.rev_tag_dict.get(rev_id), diff, signature)
 
403
                    self.rev_tag_dict.get(rev_id), diff)
475
404
                if limit:
476
405
                    log_count += 1
477
406
                    if log_count >= limit:
491
420
        else:
492
421
            specific_files = None
493
422
        s = StringIO()
494
 
        path_encoding = get_diff_header_encoding()
495
423
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
496
 
            new_label='', path_encoding=path_encoding)
 
424
            new_label='')
497
425
        return s.getvalue()
498
426
 
499
427
    def _create_log_revision_iterator(self):
523
451
        generate_merge_revisions = rqst.get('levels') != 1
524
452
        delayed_graph_generation = not rqst.get('specific_fileids') and (
525
453
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
526
 
        view_revisions = _calc_view_revisions(
527
 
            self.branch, self.start_rev_id, self.end_rev_id,
528
 
            rqst.get('direction'),
529
 
            generate_merge_revisions=generate_merge_revisions,
530
 
            delayed_graph_generation=delayed_graph_generation,
531
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
454
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
455
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
 
456
            delayed_graph_generation=delayed_graph_generation)
532
457
 
533
458
        # Apply the other filters
534
459
        return make_log_rev_iterator(self.branch, view_revisions,
535
 
            rqst.get('delta_type'), rqst.get('match'),
 
460
            rqst.get('delta_type'), rqst.get('message_search'),
536
461
            file_ids=rqst.get('specific_fileids'),
537
462
            direction=rqst.get('direction'))
538
463
 
541
466
        # Note that we always generate the merge revisions because
542
467
        # filter_revisions_touching_file_id() requires them ...
543
468
        rqst = self.rqst
544
 
        view_revisions = _calc_view_revisions(
545
 
            self.branch, self.start_rev_id, self.end_rev_id,
546
 
            rqst.get('direction'), generate_merge_revisions=True,
547
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
469
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
470
            self.end_rev_id, rqst.get('direction'), True)
548
471
        if not isinstance(view_revisions, list):
549
472
            view_revisions = list(view_revisions)
550
473
        view_revisions = _filter_revisions_touching_file_id(self.branch,
551
474
            rqst.get('specific_fileids')[0], view_revisions,
552
475
            include_merges=rqst.get('levels') != 1)
553
476
        return make_log_rev_iterator(self.branch, view_revisions,
554
 
            rqst.get('delta_type'), rqst.get('match'))
 
477
            rqst.get('delta_type'), rqst.get('message_search'))
555
478
 
556
479
 
557
480
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
558
 
                         generate_merge_revisions,
559
 
                         delayed_graph_generation=False,
560
 
                         exclude_common_ancestry=False,
561
 
                         ):
 
481
    generate_merge_revisions, delayed_graph_generation=False):
562
482
    """Calculate the revisions to view.
563
483
 
564
484
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
565
485
             a list of the same tuples.
566
486
    """
567
 
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
568
 
        raise errors.BzrCommandError(gettext(
569
 
            '--exclude-common-ancestry requires two different revisions'))
570
 
    if direction not in ('reverse', 'forward'):
571
 
        raise ValueError(gettext('invalid direction %r') % direction)
572
487
    br_revno, br_rev_id = branch.last_revision_info()
573
488
    if br_revno == 0:
574
489
        return []
575
490
 
576
 
    if (end_rev_id and start_rev_id == end_rev_id
577
 
        and (not generate_merge_revisions
578
 
             or not _has_merges(branch, end_rev_id))):
579
 
        # If a single revision is requested, check we can handle it
580
 
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
581
 
                                           br_revno)
582
 
    elif not generate_merge_revisions:
583
 
        # If we only want to see linear revisions, we can iterate ...
584
 
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
585
 
                                             direction, exclude_common_ancestry)
586
 
        if direction == 'forward':
587
 
            iter_revs = reversed(iter_revs)
 
491
    # If a single revision is requested, check we can handle it
 
492
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
 
493
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
 
494
    if generate_single_revision:
 
495
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
 
496
 
 
497
    # If we only want to see linear revisions, we can iterate ...
 
498
    if not generate_merge_revisions:
 
499
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
500
            direction)
588
501
    else:
589
 
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
590
 
                                            direction, delayed_graph_generation,
591
 
                                            exclude_common_ancestry)
592
 
        if direction == 'forward':
593
 
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
594
 
    return iter_revs
 
502
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
503
            direction, delayed_graph_generation)
595
504
 
596
505
 
597
506
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
599
508
        # It's the tip
600
509
        return [(br_rev_id, br_revno, 0)]
601
510
    else:
602
 
        revno_str = _compute_revno_str(branch, rev_id)
 
511
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
512
        revno_str = '.'.join(str(n) for n in revno)
603
513
        return [(rev_id, revno_str, 0)]
604
514
 
605
515
 
606
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction,
607
 
                             exclude_common_ancestry=False):
608
 
    result = _linear_view_revisions(
609
 
        branch, start_rev_id, end_rev_id,
610
 
        exclude_common_ancestry=exclude_common_ancestry)
 
516
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
 
517
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
611
518
    # If a start limit was given and it's not obviously an
612
519
    # ancestor of the end limit, check it before outputting anything
613
520
    if direction == 'forward' or (start_rev_id
615
522
        try:
616
523
            result = list(result)
617
524
        except _StartNotLinearAncestor:
618
 
            raise errors.BzrCommandError(gettext('Start revision not found in'
619
 
                ' left-hand history of end revision.'))
 
525
            raise errors.BzrCommandError('Start revision not found in'
 
526
                ' left-hand history of end revision.')
 
527
    if direction == 'forward':
 
528
        result = reversed(result)
620
529
    return result
621
530
 
622
531
 
623
532
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
624
 
                            delayed_graph_generation,
625
 
                            exclude_common_ancestry=False):
 
533
    delayed_graph_generation):
626
534
    # On large trees, generating the merge graph can take 30-60 seconds
627
535
    # so we delay doing it until a merge is detected, incrementally
628
536
    # returning initial (non-merge) revisions while we can.
629
 
 
630
 
    # The above is only true for old formats (<= 0.92), for newer formats, a
631
 
    # couple of seconds only should be needed to load the whole graph and the
632
 
    # other graph operations needed are even faster than that -- vila 100201
633
537
    initial_revisions = []
634
538
    if delayed_graph_generation:
635
539
        try:
636
 
            for rev_id, revno, depth in  _linear_view_revisions(
637
 
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
540
            for rev_id, revno, depth in \
 
541
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
638
542
                if _has_merges(branch, rev_id):
639
 
                    # The end_rev_id can be nested down somewhere. We need an
640
 
                    # explicit ancestry check. There is an ambiguity here as we
641
 
                    # may not raise _StartNotLinearAncestor for a revision that
642
 
                    # is an ancestor but not a *linear* one. But since we have
643
 
                    # loaded the graph to do the check (or calculate a dotted
644
 
                    # revno), we may as well accept to show the log...  We need
645
 
                    # the check only if start_rev_id is not None as all
646
 
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
647
 
                    # -- vila 20100319
648
 
                    graph = branch.repository.get_graph()
649
 
                    if (start_rev_id is not None
650
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
651
 
                        raise _StartNotLinearAncestor()
652
 
                    # Since we collected the revisions so far, we need to
653
 
                    # adjust end_rev_id.
654
543
                    end_rev_id = rev_id
655
544
                    break
656
545
                else:
657
546
                    initial_revisions.append((rev_id, revno, depth))
658
547
            else:
659
548
                # No merged revisions found
660
 
                return initial_revisions
 
549
                if direction == 'reverse':
 
550
                    return initial_revisions
 
551
                elif direction == 'forward':
 
552
                    return reversed(initial_revisions)
 
553
                else:
 
554
                    raise ValueError('invalid direction %r' % direction)
661
555
        except _StartNotLinearAncestor:
662
556
            # A merge was never detected so the lower revision limit can't
663
557
            # be nested down somewhere
664
 
            raise errors.BzrCommandError(gettext('Start revision not found in'
665
 
                ' history of end revision.'))
666
 
 
667
 
    # We exit the loop above because we encounter a revision with merges, from
668
 
    # this revision, we need to switch to _graph_view_revisions.
 
558
            raise errors.BzrCommandError('Start revision not found in'
 
559
                ' history of end revision.')
669
560
 
670
561
    # A log including nested merges is required. If the direction is reverse,
671
562
    # we rebase the initial merge depths so that the development line is
674
565
    # indented at the end seems slightly nicer in that case.
675
566
    view_revisions = chain(iter(initial_revisions),
676
567
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
677
 
                              rebase_initial_depths=(direction == 'reverse'),
678
 
                              exclude_common_ancestry=exclude_common_ancestry))
679
 
    return view_revisions
 
568
        rebase_initial_depths=direction == 'reverse'))
 
569
    if direction == 'reverse':
 
570
        return view_revisions
 
571
    elif direction == 'forward':
 
572
        # Forward means oldest first, adjusting for depth.
 
573
        view_revisions = reverse_by_depth(list(view_revisions))
 
574
        return _rebase_merge_depth(view_revisions)
 
575
    else:
 
576
        raise ValueError('invalid direction %r' % direction)
680
577
 
681
578
 
682
579
def _has_merges(branch, rev_id):
685
582
    return len(parents) > 1
686
583
 
687
584
 
688
 
def _compute_revno_str(branch, rev_id):
689
 
    """Compute the revno string from a rev_id.
690
 
 
691
 
    :return: The revno string, or None if the revision is not in the supplied
692
 
        branch.
693
 
    """
694
 
    try:
695
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
696
 
    except errors.NoSuchRevision:
697
 
        # The revision must be outside of this branch
698
 
        return None
699
 
    else:
700
 
        return '.'.join(str(n) for n in revno)
701
 
 
702
 
 
703
585
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
704
586
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
705
587
    if start_rev_id and end_rev_id:
706
 
        try:
707
 
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
708
 
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
709
 
        except errors.NoSuchRevision:
710
 
            # one or both is not in the branch; not obvious
711
 
            return False
 
588
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
589
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
712
590
        if len(start_dotted) == 1 and len(end_dotted) == 1:
713
591
            # both on mainline
714
592
            return start_dotted[0] <= end_dotted[0]
719
597
        else:
720
598
            # not obvious
721
599
            return False
722
 
    # if either start or end is not specified then we use either the first or
723
 
    # the last revision and *they* are obvious ancestors.
724
600
    return True
725
601
 
726
602
 
727
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
728
 
                           exclude_common_ancestry=False):
 
603
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
729
604
    """Calculate a sequence of revisions to view, newest to oldest.
730
605
 
731
606
    :param start_rev_id: the lower revision-id
732
607
    :param end_rev_id: the upper revision-id
733
 
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
734
 
        the iterated revisions.
735
608
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
736
609
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
737
 
        is not found walking the left-hand history
 
610
      is not found walking the left-hand history
738
611
    """
739
612
    br_revno, br_rev_id = branch.last_revision_info()
740
613
    repo = branch.repository
741
 
    graph = repo.get_graph()
742
614
    if start_rev_id is None and end_rev_id is None:
743
615
        cur_revno = br_revno
744
 
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
745
 
            (_mod_revision.NULL_REVISION,)):
 
616
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
746
617
            yield revision_id, str(cur_revno), 0
747
618
            cur_revno -= 1
748
619
    else:
749
620
        if end_rev_id is None:
750
621
            end_rev_id = br_rev_id
751
622
        found_start = start_rev_id is None
752
 
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
753
 
                (_mod_revision.NULL_REVISION,)):
754
 
            revno_str = _compute_revno_str(branch, revision_id)
 
623
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
 
624
            revno = branch.revision_id_to_dotted_revno(revision_id)
 
625
            revno_str = '.'.join(str(n) for n in revno)
755
626
            if not found_start and revision_id == start_rev_id:
756
 
                if not exclude_common_ancestry:
757
 
                    yield revision_id, revno_str, 0
 
627
                yield revision_id, revno_str, 0
758
628
                found_start = True
759
629
                break
760
630
            else:
765
635
 
766
636
 
767
637
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
768
 
                          rebase_initial_depths=True,
769
 
                          exclude_common_ancestry=False):
 
638
    rebase_initial_depths=True):
770
639
    """Calculate revisions to view including merges, newest to oldest.
771
640
 
772
641
    :param branch: the branch
776
645
      revision is found?
777
646
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
778
647
    """
779
 
    if exclude_common_ancestry:
780
 
        stop_rule = 'with-merges-without-common-ancestry'
781
 
    else:
782
 
        stop_rule = 'with-merges'
783
648
    view_revisions = branch.iter_merge_sorted_revisions(
784
649
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
785
 
        stop_rule=stop_rule)
 
650
        stop_rule="with-merges")
786
651
    if not rebase_initial_depths:
787
652
        for (rev_id, merge_depth, revno, end_of_merge
788
653
             ) in view_revisions:
799
664
                depth_adjustment = merge_depth
800
665
            if depth_adjustment:
801
666
                if merge_depth < depth_adjustment:
802
 
                    # From now on we reduce the depth adjustement, this can be
803
 
                    # surprising for users. The alternative requires two passes
804
 
                    # which breaks the fast display of the first revision
805
 
                    # though.
806
667
                    depth_adjustment = merge_depth
807
668
                merge_depth -= depth_adjustment
808
669
            yield rev_id, '.'.join(map(str, revno)), merge_depth
809
670
 
810
671
 
 
672
def calculate_view_revisions(branch, start_revision, end_revision, direction,
 
673
        specific_fileid, generate_merge_revisions):
 
674
    """Calculate the revisions to view.
 
675
 
 
676
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
 
677
             a list of the same tuples.
 
678
    """
 
679
    # This method is no longer called by the main code path.
 
680
    # It is retained for API compatibility and may be deprecated
 
681
    # soon. IGC 20090116
 
682
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
 
683
        end_revision)
 
684
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
 
685
        direction, generate_merge_revisions or specific_fileid))
 
686
    if specific_fileid:
 
687
        view_revisions = _filter_revisions_touching_file_id(branch,
 
688
            specific_fileid, view_revisions,
 
689
            include_merges=generate_merge_revisions)
 
690
    return _rebase_merge_depth(view_revisions)
 
691
 
 
692
 
811
693
def _rebase_merge_depth(view_revisions):
812
694
    """Adjust depths upwards so the top level is 0."""
813
695
    # If either the first or last revision have a merge_depth of 0, we're done
857
739
    return log_rev_iterator
858
740
 
859
741
 
860
 
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
 
742
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
861
743
    """Create a filtered iterator of log_rev_iterator matching on a regex.
862
744
 
863
745
    :param branch: The branch being logged.
864
746
    :param generate_delta: Whether to generate a delta for each revision.
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.
 
747
    :param search: A user text search string.
869
748
    :param log_rev_iterator: An input iterator containing all revisions that
870
749
        could be displayed, in lists.
871
750
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
872
751
        delta).
873
752
    """
874
 
    if match is None:
 
753
    if search is None:
875
754
        return 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):
 
755
    searchRE = re_compile_checked(search, re.IGNORECASE,
 
756
            'log message filter')
 
757
    return _filter_message_re(searchRE, log_rev_iterator)
 
758
 
 
759
 
 
760
def _filter_message_re(searchRE, log_rev_iterator):
882
761
    for revs in log_rev_iterator:
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])
 
762
        new_revs = []
 
763
        for (rev_id, revno, merge_depth), rev, delta in revs:
 
764
            if searchRE.search(rev.message):
 
765
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
 
766
        yield new_revs
 
767
 
903
768
 
904
769
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
905
770
    fileids=None, direction='reverse'):
978
843
 
979
844
def _update_fileids(delta, fileids, stop_on):
980
845
    """Update the set of file-ids to search based on file lifecycle events.
981
 
 
 
846
    
982
847
    :param fileids: a set of fileids to update
983
848
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
984
849
      fileids set once their add or remove entry is detected respectively
1025
890
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1026
891
        delta).
1027
892
    """
 
893
    repository = branch.repository
1028
894
    num = 9
1029
895
    for batch in log_rev_iterator:
1030
896
        batch = iter(batch)
1079
945
    if branch_revno != 0:
1080
946
        if (start_rev_id == _mod_revision.NULL_REVISION
1081
947
            or end_rev_id == _mod_revision.NULL_REVISION):
1082
 
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
948
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
1083
949
        if start_revno > end_revno:
1084
 
            raise errors.BzrCommandError(gettext("Start revision must be "
1085
 
                                         "older than the end revision."))
 
950
            raise errors.BzrCommandError("Start revision must be older than "
 
951
                                         "the end revision.")
1086
952
    return (start_rev_id, end_rev_id)
1087
953
 
1088
954
 
1137
1003
 
1138
1004
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1139
1005
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1140
 
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1006
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
1141
1007
    if start_revno > end_revno:
1142
 
        raise errors.BzrCommandError(gettext("Start revision must be older "
1143
 
                                     "than the end revision."))
 
1008
        raise errors.BzrCommandError("Start revision must be older than "
 
1009
                                     "the end revision.")
1144
1010
 
1145
1011
    if end_revno < start_revno:
1146
1012
        return None, None, None, None
1147
1013
    cur_revno = branch_revno
1148
1014
    rev_nos = {}
1149
1015
    mainline_revs = []
1150
 
    graph = branch.repository.get_graph()
1151
 
    for revision_id in graph.iter_lefthand_ancestry(
1152
 
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1016
    for revision_id in branch.repository.iter_reverse_revision_history(
 
1017
                        branch_last_revision):
1153
1018
        if cur_revno < start_revno:
1154
1019
            # We have gone far enough, but we always add 1 more revision
1155
1020
            rev_nos[revision_id] = cur_revno
1169
1034
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1170
1035
 
1171
1036
 
 
1037
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
 
1038
    """Filter view_revisions based on revision ranges.
 
1039
 
 
1040
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
 
1041
            tuples to be filtered.
 
1042
 
 
1043
    :param start_rev_id: If not NONE specifies the first revision to be logged.
 
1044
            If NONE then all revisions up to the end_rev_id are logged.
 
1045
 
 
1046
    :param end_rev_id: If not NONE specifies the last revision to be logged.
 
1047
            If NONE then all revisions up to the end of the log are logged.
 
1048
 
 
1049
    :return: The filtered view_revisions.
 
1050
    """
 
1051
    # This method is no longer called by the main code path.
 
1052
    # It may be removed soon. IGC 20090127
 
1053
    if start_rev_id or end_rev_id:
 
1054
        revision_ids = [r for r, n, d in view_revisions]
 
1055
        if start_rev_id:
 
1056
            start_index = revision_ids.index(start_rev_id)
 
1057
        else:
 
1058
            start_index = 0
 
1059
        if start_rev_id == end_rev_id:
 
1060
            end_index = start_index
 
1061
        else:
 
1062
            if end_rev_id:
 
1063
                end_index = revision_ids.index(end_rev_id)
 
1064
            else:
 
1065
                end_index = len(view_revisions) - 1
 
1066
        # To include the revisions merged into the last revision,
 
1067
        # extend end_rev_id down to, but not including, the next rev
 
1068
        # with the same or lesser merge_depth
 
1069
        end_merge_depth = view_revisions[end_index][2]
 
1070
        try:
 
1071
            for index in xrange(end_index+1, len(view_revisions)+1):
 
1072
                if view_revisions[index][2] <= end_merge_depth:
 
1073
                    end_index = index - 1
 
1074
                    break
 
1075
        except IndexError:
 
1076
            # if the search falls off the end then log to the end as well
 
1077
            end_index = len(view_revisions) - 1
 
1078
        view_revisions = view_revisions[start_index:end_index+1]
 
1079
    return view_revisions
 
1080
 
 
1081
 
1172
1082
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1173
1083
    include_merges=True):
1174
1084
    r"""Return the list of revision ids which touch a given file id.
1177
1087
    This includes the revisions which directly change the file id,
1178
1088
    and the revisions which merge these changes. So if the
1179
1089
    revision graph is::
1180
 
 
1181
1090
        A-.
1182
1091
        |\ \
1183
1092
        B C E
1210
1119
    """
1211
1120
    # Lookup all possible text keys to determine which ones actually modified
1212
1121
    # the file.
1213
 
    graph = branch.repository.get_file_graph()
1214
 
    get_parent_map = graph.get_parent_map
1215
1122
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1216
1123
    next_keys = None
1217
1124
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1221
1128
    #       indexing layer. We might consider passing in hints as to the known
1222
1129
    #       access pattern (sparse/clustered, high success rate/low success
1223
1130
    #       rate). This particular access is clustered with a low success rate.
 
1131
    get_parent_map = branch.repository.texts.get_parent_map
1224
1132
    modified_text_revisions = set()
1225
1133
    chunk_size = 1000
1226
1134
    for start in xrange(0, len(text_keys), chunk_size):
1253
1161
    return result
1254
1162
 
1255
1163
 
 
1164
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
 
1165
                       include_merges=True):
 
1166
    """Produce an iterator of revisions to show
 
1167
    :return: an iterator of (revision_id, revno, merge_depth)
 
1168
    (if there is no revno for a revision, None is supplied)
 
1169
    """
 
1170
    # This method is no longer called by the main code path.
 
1171
    # It is retained for API compatibility and may be deprecated
 
1172
    # soon. IGC 20090127
 
1173
    if not include_merges:
 
1174
        revision_ids = mainline_revs[1:]
 
1175
        if direction == 'reverse':
 
1176
            revision_ids.reverse()
 
1177
        for revision_id in revision_ids:
 
1178
            yield revision_id, str(rev_nos[revision_id]), 0
 
1179
        return
 
1180
    graph = branch.repository.get_graph()
 
1181
    # This asks for all mainline revisions, which means we only have to spider
 
1182
    # sideways, rather than depth history. That said, its still size-of-history
 
1183
    # and should be addressed.
 
1184
    # mainline_revisions always includes an extra revision at the beginning, so
 
1185
    # don't request it.
 
1186
    parent_map = dict(((key, value) for key, value in
 
1187
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
 
1188
    # filter out ghosts; merge_sort errors on ghosts.
 
1189
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
 
1190
    merge_sorted_revisions = tsort.merge_sort(
 
1191
        rev_graph,
 
1192
        mainline_revs[-1],
 
1193
        mainline_revs,
 
1194
        generate_revno=True)
 
1195
 
 
1196
    if direction == 'forward':
 
1197
        # forward means oldest first.
 
1198
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
 
1199
    elif direction != 'reverse':
 
1200
        raise ValueError('invalid direction %r' % direction)
 
1201
 
 
1202
    for (sequence, rev_id, merge_depth, revno, end_of_merge
 
1203
         ) in merge_sorted_revisions:
 
1204
        yield rev_id, '.'.join(map(str, revno)), merge_depth
 
1205
 
 
1206
 
1256
1207
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1257
1208
    """Reverse revisions by depth.
1258
1209
 
1293
1244
    """
1294
1245
 
1295
1246
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1296
 
                 tags=None, diff=None, signature=None):
 
1247
                 tags=None, diff=None):
1297
1248
        self.rev = rev
1298
 
        if revno is None:
1299
 
            self.revno = None
1300
 
        else:
1301
 
            self.revno = str(revno)
 
1249
        self.revno = str(revno)
1302
1250
        self.merge_depth = merge_depth
1303
1251
        self.delta = delta
1304
1252
        self.tags = tags
1305
1253
        self.diff = diff
1306
 
        self.signature = signature
1307
1254
 
1308
1255
 
1309
1256
class LogFormatter(object):
1318
1265
    to indicate which LogRevision attributes it supports:
1319
1266
 
1320
1267
    - supports_delta must be True if this log formatter supports delta.
1321
 
      Otherwise the delta attribute may not be populated.  The 'delta_format'
1322
 
      attribute describes whether the 'short_status' format (1) or the long
1323
 
      one (2) should be used.
 
1268
        Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1269
        attribute describes whether the 'short_status' format (1) or the long
 
1270
        one (2) should be used.
1324
1271
 
1325
1272
    - supports_merge_revisions must be True if this log formatter supports
1326
 
      merge revisions.  If not, then only mainline revisions will be passed
1327
 
      to the formatter.
 
1273
        merge revisions.  If not, then only mainline revisions will be passed
 
1274
        to the formatter.
1328
1275
 
1329
1276
    - preferred_levels is the number of levels this formatter defaults to.
1330
 
      The default value is zero meaning display all levels.
1331
 
      This value is only relevant if supports_merge_revisions is True.
 
1277
        The default value is zero meaning display all levels.
 
1278
        This value is only relevant if supports_merge_revisions is True.
1332
1279
 
1333
1280
    - supports_tags must be True if this log formatter supports tags.
1334
 
      Otherwise the tags attribute may not be populated.
 
1281
        Otherwise the tags attribute may not be populated.
1335
1282
 
1336
1283
    - supports_diff must be True if this log formatter supports diffs.
1337
 
      Otherwise the diff attribute may not be populated.
1338
 
 
1339
 
    - supports_signatures must be True if this log formatter supports GPG
1340
 
      signatures.
 
1284
        Otherwise the diff attribute may not be populated.
1341
1285
 
1342
1286
    Plugins can register functions to show custom revision properties using
1343
1287
    the properties_handler_registry. The registered function
1344
 
    must respect the following interface description::
1345
 
 
 
1288
    must respect the following interface description:
1346
1289
        def my_show_properties(properties_dict):
1347
1290
            # code that returns a dict {'name':'value'} of the properties
1348
1291
            # to be shown
1350
1293
    preferred_levels = 0
1351
1294
 
1352
1295
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1353
 
                 delta_format=None, levels=None, show_advice=False,
1354
 
                 to_exact_file=None, author_list_handler=None):
 
1296
            delta_format=None, levels=None, show_advice=False,
 
1297
            to_exact_file=None):
1355
1298
        """Create a LogFormatter.
1356
1299
 
1357
1300
        :param to_file: the file to output to
1358
 
        :param to_exact_file: if set, gives an output stream to which
 
1301
        :param to_exact_file: if set, gives an output stream to which 
1359
1302
             non-Unicode diffs are written.
1360
1303
        :param show_ids: if True, revision-ids are to be displayed
1361
1304
        :param show_timezone: the timezone to use
1365
1308
          let the log formatter decide.
1366
1309
        :param show_advice: whether to show advice at the end of the
1367
1310
          log or not
1368
 
        :param author_list_handler: callable generating a list of
1369
 
          authors to display for a given revision
1370
1311
        """
1371
1312
        self.to_file = to_file
1372
1313
        # 'exact' stream used to show diff, it should print content 'as is'
1387
1328
        self.levels = levels
1388
1329
        self._show_advice = show_advice
1389
1330
        self._merge_count = 0
1390
 
        self._author_list_handler = author_list_handler
1391
1331
 
1392
1332
    def get_levels(self):
1393
1333
        """Get the number of levels to display or 0 for all."""
1412
1352
            if advice_sep:
1413
1353
                self.to_file.write(advice_sep)
1414
1354
            self.to_file.write(
1415
 
                "Use --include-merged or -n0 to see merged revisions.\n")
 
1355
                "Use --include-merges or -n0 to see merged revisions.\n")
1416
1356
 
1417
1357
    def get_advice_separator(self):
1418
1358
        """Get the text separating the log from the closing advice."""
1425
1365
        return address
1426
1366
 
1427
1367
    def short_author(self, rev):
1428
 
        return self.authors(rev, 'first', short=True, sep=', ')
1429
 
 
1430
 
    def authors(self, rev, who, short=False, sep=None):
1431
 
        """Generate list of authors, taking --authors option into account.
1432
 
 
1433
 
        The caller has to specify the name of a author list handler,
1434
 
        as provided by the author list registry, using the ``who``
1435
 
        argument.  That name only sets a default, though: when the
1436
 
        user selected a different author list generation using the
1437
 
        ``--authors`` command line switch, as represented by the
1438
 
        ``author_list_handler`` constructor argument, that value takes
1439
 
        precedence.
1440
 
 
1441
 
        :param rev: The revision for which to generate the list of authors.
1442
 
        :param who: Name of the default handler.
1443
 
        :param short: Whether to shorten names to either name or address.
1444
 
        :param sep: What separator to use for automatic concatenation.
1445
 
        """
1446
 
        if self._author_list_handler is not None:
1447
 
            # The user did specify --authors, which overrides the default
1448
 
            author_list_handler = self._author_list_handler
1449
 
        else:
1450
 
            # The user didn't specify --authors, so we use the caller's default
1451
 
            author_list_handler = author_list_registry.get(who)
1452
 
        names = author_list_handler(rev)
1453
 
        if short:
1454
 
            for i in range(len(names)):
1455
 
                name, address = config.parse_username(names[i])
1456
 
                if name:
1457
 
                    names[i] = name
1458
 
                else:
1459
 
                    names[i] = address
1460
 
        if sep is not None:
1461
 
            names = sep.join(names)
1462
 
        return names
 
1368
        name, address = config.parse_username(rev.get_apparent_authors()[0])
 
1369
        if name:
 
1370
            return name
 
1371
        return address
1463
1372
 
1464
1373
    def merge_marker(self, revision):
1465
1374
        """Get the merge marker to include in the output or '' if none."""
1496
1405
        """
1497
1406
        # Revision comes directly from a foreign repository
1498
1407
        if isinstance(rev, foreign.ForeignRevision):
1499
 
            return self._format_properties(
1500
 
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1408
            return rev.mapping.vcs.show_foreign_revid(rev.foreign_revid)
1501
1409
 
1502
1410
        # Imported foreign revision revision ids always contain :
1503
1411
        if not ":" in rev.revision_id:
1535
1443
    supports_delta = True
1536
1444
    supports_tags = True
1537
1445
    supports_diff = True
1538
 
    supports_signatures = True
1539
1446
 
1540
1447
    def __init__(self, *args, **kwargs):
1541
1448
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1561
1468
                self.merge_marker(revision)))
1562
1469
        if revision.tags:
1563
1470
            lines.append('tags: %s' % (', '.join(revision.tags)))
1564
 
        if self.show_ids or revision.revno is None:
 
1471
        if self.show_ids:
1565
1472
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1566
 
        if self.show_ids:
1567
1473
            for parent_id in revision.rev.parent_ids:
1568
1474
                lines.append('parent: %s' % (parent_id,))
1569
1475
        lines.extend(self.custom_properties(revision.rev))
1570
1476
 
1571
1477
        committer = revision.rev.committer
1572
 
        authors = self.authors(revision.rev, 'all')
 
1478
        authors = revision.rev.get_apparent_authors()
1573
1479
        if authors != [committer]:
1574
1480
            lines.append('author: %s' % (", ".join(authors),))
1575
1481
        lines.append('committer: %s' % (committer,))
1580
1486
 
1581
1487
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1582
1488
 
1583
 
        if revision.signature is not None:
1584
 
            lines.append('signature: ' + revision.signature)
1585
 
 
1586
1489
        lines.append('message:')
1587
1490
        if not revision.rev.message:
1588
1491
            lines.append('  (no message)')
1595
1498
        to_file = self.to_file
1596
1499
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1597
1500
        if revision.delta is not None:
1598
 
            # Use the standard status output to display changes
1599
 
            from bzrlib.delta import report_delta
1600
 
            report_delta(to_file, revision.delta, short_status=False,
1601
 
                         show_ids=self.show_ids, indent=indent)
 
1501
            # We don't respect delta_format for compatibility
 
1502
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1503
                                short_status=False)
1602
1504
        if revision.diff is not None:
1603
1505
            to_file.write(indent + 'diff:\n')
1604
1506
            to_file.flush()
1635
1537
        indent = '    ' * depth
1636
1538
        revno_width = self.revno_width_by_depth.get(depth)
1637
1539
        if revno_width is None:
1638
 
            if revision.revno is None or revision.revno.find('.') == -1:
 
1540
            if revision.revno.find('.') == -1:
1639
1541
                # mainline revno, e.g. 12345
1640
1542
                revno_width = 5
1641
1543
            else:
1649
1551
        if revision.tags:
1650
1552
            tags = ' {%s}' % (', '.join(revision.tags))
1651
1553
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1652
 
                revision.revno or "", self.short_author(revision.rev),
 
1554
                revision.revno, self.short_author(revision.rev),
1653
1555
                format_date(revision.rev.timestamp,
1654
1556
                            revision.rev.timezone or 0,
1655
1557
                            self.show_timezone, date_fmt="%Y-%m-%d",
1656
1558
                            show_offset=False),
1657
1559
                tags, self.merge_marker(revision)))
1658
1560
        self.show_properties(revision.rev, indent+offset)
1659
 
        if self.show_ids or revision.revno is None:
 
1561
        if self.show_ids:
1660
1562
            to_file.write(indent + offset + 'revision-id:%s\n'
1661
1563
                          % (revision.rev.revision_id,))
1662
1564
        if not revision.rev.message:
1667
1569
                to_file.write(indent + offset + '%s\n' % (l,))
1668
1570
 
1669
1571
        if revision.delta is not None:
1670
 
            # Use the standard status output to display changes
1671
 
            from bzrlib.delta import report_delta
1672
 
            report_delta(to_file, revision.delta,
1673
 
                         short_status=self.delta_format==1,
1674
 
                         show_ids=self.show_ids, indent=indent + offset)
 
1572
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1573
                                short_status=self.delta_format==1)
1675
1574
        if revision.diff is not None:
1676
1575
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1677
1576
        to_file.write('\n')
1715
1614
 
1716
1615
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1717
1616
        """Format log info into one string. Truncate tail of string
1718
 
 
1719
 
        :param revno:      revision number or None.
1720
 
                           Revision numbers counts from 1.
1721
 
        :param rev:        revision object
1722
 
        :param max_chars:  maximum length of resulting string
1723
 
        :param tags:       list of tags or None
1724
 
        :param prefix:     string to prefix each line
1725
 
        :return:           formatted truncated string
 
1617
        :param  revno:      revision number or None.
 
1618
                            Revision numbers counts from 1.
 
1619
        :param  rev:        revision object
 
1620
        :param  max_chars:  maximum length of resulting string
 
1621
        :param  tags:       list of tags or None
 
1622
        :param  prefix:     string to prefix each line
 
1623
        :return:            formatted truncated string
1726
1624
        """
1727
1625
        out = []
1728
1626
        if revno:
1729
1627
            # show revno only when is not None
1730
1628
            out.append("%s:" % revno)
1731
 
        if max_chars is not None:
1732
 
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1733
 
        else:
1734
 
            out.append(self.short_author(rev))
 
1629
        out.append(self.truncate(self.short_author(rev), 20))
1735
1630
        out.append(self.date_string(rev))
1736
1631
        if len(rev.parent_ids) > 1:
1737
1632
            out.append('[merge]')
1756
1651
                               self.show_timezone,
1757
1652
                               date_fmt='%Y-%m-%d',
1758
1653
                               show_offset=False)
1759
 
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1760
 
        committer_str = committer_str.replace(' <', '  <')
 
1654
        committer_str = revision.rev.committer.replace (' <', '  <')
1761
1655
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1762
1656
 
1763
1657
        if revision.delta is not None and revision.delta.has_changed():
1825
1719
    try:
1826
1720
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1827
1721
    except KeyError:
1828
 
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1829
 
 
1830
 
 
1831
 
def author_list_all(rev):
1832
 
    return rev.get_apparent_authors()[:]
1833
 
 
1834
 
 
1835
 
def author_list_first(rev):
1836
 
    lst = rev.get_apparent_authors()
1837
 
    try:
1838
 
        return [lst[0]]
1839
 
    except IndexError:
1840
 
        return []
1841
 
 
1842
 
 
1843
 
def author_list_committer(rev):
1844
 
    return [rev.committer]
1845
 
 
1846
 
 
1847
 
author_list_registry = registry.Registry()
1848
 
 
1849
 
author_list_registry.register('all', author_list_all,
1850
 
                              'All authors')
1851
 
 
1852
 
author_list_registry.register('first', author_list_first,
1853
 
                              'The first author')
1854
 
 
1855
 
author_list_registry.register('committer', author_list_committer,
1856
 
                              'The committer')
 
1722
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1723
 
 
1724
 
 
1725
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
 
1726
    # deprecated; for compatibility
 
1727
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
 
1728
    lf.show(revno, rev, delta)
1857
1729
 
1858
1730
 
1859
1731
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1924
1796
    old_revisions = set()
1925
1797
    new_history = []
1926
1798
    new_revisions = set()
1927
 
    graph = repository.get_graph()
1928
 
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
1929
 
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
 
1799
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
 
1800
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
1930
1801
    stop_revision = None
1931
1802
    do_old = True
1932
1803
    do_new = True
2007
1878
        lf.log_revision(lr)
2008
1879
 
2009
1880
 
2010
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
1881
def _get_info_for_log_files(revisionspec_list, file_list):
2011
1882
    """Find file-ids and kinds given a list of files and a revision range.
2012
1883
 
2013
1884
    We search for files at the end of the range. If not found there,
2017
1888
    :param file_list: the list of paths given on the command line;
2018
1889
      the first of these can be a branch location or a file path,
2019
1890
      the remainder must be file paths
2020
 
    :param add_cleanup: When the branch returned is read locked,
2021
 
      an unlock call will be queued to the cleanup.
2022
1891
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2023
1892
      info_list is a list of (relative_path, file_id, kind) tuples where
2024
1893
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2025
1894
      branch will be read-locked.
2026
1895
    """
2027
 
    from builtins import _get_revision_range
 
1896
    from builtins import _get_revision_range, safe_relpath_files
2028
1897
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
2029
 
    add_cleanup(b.lock_read().unlock)
 
1898
    b.lock_read()
2030
1899
    # XXX: It's damn messy converting a list of paths to relative paths when
2031
1900
    # those paths might be deleted ones, they might be on a case-insensitive
2032
1901
    # filesystem and/or they might be in silly locations (like another branch).
2036
1905
    # case of running log in a nested directory, assuming paths beyond the
2037
1906
    # first one haven't been deleted ...
2038
1907
    if tree:
2039
 
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
1908
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2040
1909
    else:
2041
1910
        relpaths = [path] + file_list[1:]
2042
1911
    info_list = []
2111
1980
 
2112
1981
properties_handler_registry = registry.Registry()
2113
1982
 
2114
 
# Use the properties handlers to print out bug information if available
2115
 
def _bugs_properties_handler(revision):
2116
 
    if revision.properties.has_key('bugs'):
2117
 
        bug_lines = revision.properties['bugs'].split('\n')
2118
 
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2119
 
        fixed_bug_urls = [row[0] for row in bug_rows if
2120
 
                          len(row) > 1 and row[1] == 'fixed']
2121
 
 
2122
 
        if fixed_bug_urls:
2123
 
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
2124
 
                    ' '.join(fixed_bug_urls)}
2125
 
    return {}
2126
 
 
2127
 
properties_handler_registry.register('bugs_properties_handler',
2128
 
                                     _bugs_properties_handler)
2129
 
 
2130
1983
 
2131
1984
# adapters which revision ids to log are filtered. When log is called, the
2132
1985
# log_rev_iterator is adapted through each of these factory methods.