~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

(vila) Fix test failures blocking package builds. (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2009 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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
 
18
 
 
19
17
"""Code to show logs of changes.
20
18
 
21
19
Various flavors of log can be produced:
49
47
all the changes since the previous revision that touched hello.c.
50
48
"""
51
49
 
 
50
from __future__ import absolute_import
 
51
 
52
52
import codecs
53
53
from cStringIO import StringIO
54
54
from itertools import (
65
65
lazy_import(globals(), """
66
66
 
67
67
from bzrlib import (
68
 
    bzrdir,
69
68
    config,
 
69
    controldir,
70
70
    diff,
71
71
    errors,
72
72
    foreign,
73
73
    repository as _mod_repository,
74
74
    revision as _mod_revision,
75
75
    revisionspec,
76
 
    trace,
77
76
    tsort,
78
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
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,
 
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
86
89
    get_terminal_encoding,
87
 
    re_compile_checked,
88
90
    terminal_width,
89
91
    )
90
92
 
103
105
    last_ie = None
104
106
    last_path = None
105
107
    revno = 1
106
 
    for revision_id in branch.revision_history():
107
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
108
 
        if file_id in this_inv:
 
108
    graph = branch.repository.get_graph()
 
109
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
110
        [_mod_revision.NULL_REVISION]))
 
111
    for revision_id in reversed(history):
 
112
        this_inv = branch.repository.get_inventory(revision_id)
 
113
        if this_inv.has_id(file_id):
109
114
            this_ie = this_inv[file_id]
110
115
            this_path = this_inv.id2path(file_id)
111
116
        else:
133
138
        revno += 1
134
139
 
135
140
 
136
 
def _enumerate_history(branch):
137
 
    rh = []
138
 
    revno = 1
139
 
    for rev_id in branch.revision_history():
140
 
        rh.append((revno, rev_id))
141
 
        revno += 1
142
 
    return rh
143
 
 
144
 
 
145
141
def show_log(branch,
146
142
             lf,
147
143
             specific_fileid=None,
151
147
             end_revision=None,
152
148
             search=None,
153
149
             limit=None,
154
 
             show_diff=False):
 
150
             show_diff=False,
 
151
             match=None):
155
152
    """Write out human-readable log of commits to this branch.
156
153
 
157
154
    This function is being retained for backwards compatibility but
180
177
        if None or 0.
181
178
 
182
179
    :param show_diff: If True, output a diff after each revision.
 
180
 
 
181
    :param match: Dictionary of search lists to use when matching revision
 
182
      properties.
183
183
    """
184
184
    # Convert old-style parameters to new-style parameters
185
185
    if specific_fileid is not None:
209
209
    Logger(branch, rqst).show(lf)
210
210
 
211
211
 
212
 
# Note: This needs to be kept this in sync with the defaults in
 
212
# Note: This needs to be kept in sync with the defaults in
213
213
# make_log_request_dict() below
214
214
_DEFAULT_REQUEST_PARAMS = {
215
215
    'direction': 'reverse',
216
 
    'levels': 1,
 
216
    'levels': None,
217
217
    'generate_tags': True,
 
218
    'exclude_common_ancestry': False,
218
219
    '_match_using_deltas': True,
219
220
    }
220
221
 
221
222
 
222
223
def make_log_request_dict(direction='reverse', specific_fileids=None,
223
 
    start_revision=None, end_revision=None, limit=None,
224
 
    message_search=None, levels=1, generate_tags=True, delta_type=None,
225
 
    diff_type=None, _match_using_deltas=True):
 
224
                          start_revision=None, end_revision=None, limit=None,
 
225
                          message_search=None, levels=None, generate_tags=True,
 
226
                          delta_type=None,
 
227
                          diff_type=None, _match_using_deltas=True,
 
228
                          exclude_common_ancestry=False, match=None,
 
229
                          signature=False, omit_merges=False,
 
230
                          ):
226
231
    """Convenience function for making a logging request dictionary.
227
232
 
228
233
    Using this function may make code slightly safer by ensuring
248
253
      matching commit messages
249
254
 
250
255
    :param levels: the number of levels of revisions to
251
 
      generate; 1 for just the mainline; 0 for all levels.
 
256
      generate; 1 for just the mainline; 0 for all levels, or None for
 
257
      a sensible default.
252
258
 
253
259
    :param generate_tags: If True, include tags for matched revisions.
254
 
 
 
260
`
255
261
    :param delta_type: Either 'full', 'partial' or None.
256
262
      'full' means generate the complete delta - adds/deletes/modifies/etc;
257
263
      'partial' means filter the delta using specific_fileids;
266
272
      algorithm used for matching specific_fileids. This parameter
267
273
      may be removed in the future so bzrlib client code should NOT
268
274
      use it.
 
275
 
 
276
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
 
277
      range operator or as a graph difference.
 
278
 
 
279
    :param signature: show digital signature information
 
280
 
 
281
    :param match: Dictionary of list of search strings to use when filtering
 
282
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
283
      the empty string to match any of the preceding properties.
 
284
 
 
285
    :param omit_merges: If True, commits with more than one parent are
 
286
      omitted.
 
287
 
269
288
    """
 
289
    # Take care of old style message_search parameter
 
290
    if message_search:
 
291
        if match:
 
292
            if 'message' in match:
 
293
                match['message'].append(message_search)
 
294
            else:
 
295
                match['message'] = [message_search]
 
296
        else:
 
297
            match={ 'message': [message_search] }
270
298
    return {
271
299
        'direction': direction,
272
300
        'specific_fileids': specific_fileids,
273
301
        'start_revision': start_revision,
274
302
        'end_revision': end_revision,
275
303
        'limit': limit,
276
 
        'message_search': message_search,
277
304
        'levels': levels,
278
305
        'generate_tags': generate_tags,
279
306
        'delta_type': delta_type,
280
307
        'diff_type': diff_type,
 
308
        'exclude_common_ancestry': exclude_common_ancestry,
 
309
        'signature': signature,
 
310
        'match': match,
 
311
        'omit_merges': omit_merges,
281
312
        # Add 'private' attributes for features that may be deprecated
282
313
        '_match_using_deltas': _match_using_deltas,
283
314
    }
285
316
 
286
317
def _apply_log_request_defaults(rqst):
287
318
    """Apply default values to a request dictionary."""
288
 
    result = _DEFAULT_REQUEST_PARAMS
 
319
    result = _DEFAULT_REQUEST_PARAMS.copy()
289
320
    if rqst:
290
321
        result.update(rqst)
291
322
    return result
292
323
 
293
324
 
 
325
def format_signature_validity(rev_id, repo):
 
326
    """get the signature validity
 
327
 
 
328
    :param rev_id: revision id to validate
 
329
    :param repo: repository of revision
 
330
    :return: human readable string to print to log
 
331
    """
 
332
    from bzrlib import gpg
 
333
 
 
334
    gpg_strategy = gpg.GPGStrategy(None)
 
335
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
 
336
    if result[0] == gpg.SIGNATURE_VALID:
 
337
        return u"valid signature from {0}".format(result[1])
 
338
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
339
        return "unknown key {0}".format(result[1])
 
340
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
341
        return "invalid signature!"
 
342
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
343
        return "no signature"
 
344
 
 
345
 
294
346
class LogGenerator(object):
295
347
    """A generator of log revisions."""
296
348
 
303
355
 
304
356
 
305
357
class Logger(object):
306
 
    """An object the generates, formats and displays a log."""
 
358
    """An object that generates, formats and displays a log."""
307
359
 
308
360
    def __init__(self, branch, rqst):
309
361
        """Create a Logger.
341
393
        # Tweak the LogRequest based on what the LogFormatter can handle.
342
394
        # (There's no point generating stuff if the formatter can't display it.)
343
395
        rqst = self.rqst
344
 
        rqst['levels'] = lf.get_levels()
 
396
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
397
            # user didn't specify levels, use whatever the LF can handle:
 
398
            rqst['levels'] = lf.get_levels()
 
399
 
345
400
        if not getattr(lf, 'supports_tags', False):
346
401
            rqst['generate_tags'] = False
347
402
        if not getattr(lf, 'supports_delta', False):
348
403
            rqst['delta_type'] = None
349
404
        if not getattr(lf, 'supports_diff', False):
350
405
            rqst['diff_type'] = None
 
406
        if not getattr(lf, 'supports_signatures', False):
 
407
            rqst['signature'] = False
351
408
 
352
409
        # Find and print the interesting revisions
353
410
        generator = self._generator_factory(self.branch, rqst)
357
414
 
358
415
    def _generator_factory(self, branch, rqst):
359
416
        """Make the LogGenerator object to use.
360
 
        
 
417
 
361
418
        Subclasses may wish to override this.
362
419
        """
363
420
        return _DefaultLogGenerator(branch, rqst)
384
441
        :return: An iterator yielding LogRevision objects.
385
442
        """
386
443
        rqst = self.rqst
 
444
        levels = rqst.get('levels')
 
445
        limit = rqst.get('limit')
 
446
        diff_type = rqst.get('diff_type')
 
447
        show_signature = rqst.get('signature')
 
448
        omit_merges = rqst.get('omit_merges')
387
449
        log_count = 0
388
450
        revision_iterator = self._create_log_revision_iterator()
389
451
        for revs in revision_iterator:
390
452
            for (rev_id, revno, merge_depth), rev, delta in revs:
391
453
                # 0 levels means show everything; merge_depth counts from 0
392
 
                levels = rqst.get('levels')
393
454
                if levels != 0 and merge_depth >= levels:
394
455
                    continue
395
 
                diff = self._format_diff(rev, rev_id)
 
456
                if omit_merges and len(rev.parent_ids) > 1:
 
457
                    continue
 
458
                if diff_type is None:
 
459
                    diff = None
 
460
                else:
 
461
                    diff = self._format_diff(rev, rev_id, diff_type)
 
462
                if show_signature:
 
463
                    signature = format_signature_validity(rev_id,
 
464
                                                self.branch.repository)
 
465
                else:
 
466
                    signature = None
396
467
                yield LogRevision(rev, revno, merge_depth, delta,
397
 
                    self.rev_tag_dict.get(rev_id), diff)
398
 
                limit = rqst.get('limit')
 
468
                    self.rev_tag_dict.get(rev_id), diff, signature)
399
469
                if limit:
400
470
                    log_count += 1
401
471
                    if log_count >= limit:
402
472
                        return
403
473
 
404
 
    def _format_diff(self, rev, rev_id):
405
 
        diff_type = self.rqst.get('diff_type')
406
 
        if diff_type is None:
407
 
            return None
 
474
    def _format_diff(self, rev, rev_id, diff_type):
408
475
        repo = self.branch.repository
409
476
        if len(rev.parent_ids) == 0:
410
477
            ancestor_id = _mod_revision.NULL_REVISION
418
485
        else:
419
486
            specific_files = None
420
487
        s = StringIO()
 
488
        path_encoding = get_diff_header_encoding()
421
489
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
422
 
            new_label='')
 
490
            new_label='', path_encoding=path_encoding)
423
491
        return s.getvalue()
424
492
 
425
493
    def _create_log_revision_iterator(self):
449
517
        generate_merge_revisions = rqst.get('levels') != 1
450
518
        delayed_graph_generation = not rqst.get('specific_fileids') and (
451
519
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
452
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
453
 
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
454
 
            delayed_graph_generation=delayed_graph_generation)
 
520
        view_revisions = _calc_view_revisions(
 
521
            self.branch, self.start_rev_id, self.end_rev_id,
 
522
            rqst.get('direction'),
 
523
            generate_merge_revisions=generate_merge_revisions,
 
524
            delayed_graph_generation=delayed_graph_generation,
 
525
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
455
526
 
456
527
        # Apply the other filters
457
528
        return make_log_rev_iterator(self.branch, view_revisions,
458
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
529
            rqst.get('delta_type'), rqst.get('match'),
459
530
            file_ids=rqst.get('specific_fileids'),
460
531
            direction=rqst.get('direction'))
461
532
 
464
535
        # Note that we always generate the merge revisions because
465
536
        # filter_revisions_touching_file_id() requires them ...
466
537
        rqst = self.rqst
467
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
468
 
            self.end_rev_id, rqst.get('direction'), True)
 
538
        view_revisions = _calc_view_revisions(
 
539
            self.branch, self.start_rev_id, self.end_rev_id,
 
540
            rqst.get('direction'), generate_merge_revisions=True,
 
541
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
469
542
        if not isinstance(view_revisions, list):
470
543
            view_revisions = list(view_revisions)
471
544
        view_revisions = _filter_revisions_touching_file_id(self.branch,
472
545
            rqst.get('specific_fileids')[0], view_revisions,
473
546
            include_merges=rqst.get('levels') != 1)
474
547
        return make_log_rev_iterator(self.branch, view_revisions,
475
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
548
            rqst.get('delta_type'), rqst.get('match'))
476
549
 
477
550
 
478
551
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
479
 
    generate_merge_revisions, delayed_graph_generation=False):
 
552
                         generate_merge_revisions,
 
553
                         delayed_graph_generation=False,
 
554
                         exclude_common_ancestry=False,
 
555
                         ):
480
556
    """Calculate the revisions to view.
481
557
 
482
558
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
483
559
             a list of the same tuples.
484
560
    """
 
561
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
 
562
        raise errors.BzrCommandError(gettext(
 
563
            '--exclude-common-ancestry requires two different revisions'))
 
564
    if direction not in ('reverse', 'forward'):
 
565
        raise ValueError(gettext('invalid direction %r') % direction)
485
566
    br_revno, br_rev_id = branch.last_revision_info()
486
567
    if br_revno == 0:
487
568
        return []
488
569
 
489
 
    # If a single revision is requested, check we can handle it
490
 
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
491
 
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
492
 
    if generate_single_revision:
493
 
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
494
 
 
495
 
    # If we only want to see linear revisions, we can iterate ...
 
570
    if (end_rev_id and start_rev_id == end_rev_id
 
571
        and (not generate_merge_revisions
 
572
             or not _has_merges(branch, end_rev_id))):
 
573
        # If a single revision is requested, check we can handle it
 
574
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
575
                                       br_revno)
496
576
    if not generate_merge_revisions:
497
 
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
498
 
            direction)
499
 
    else:
500
 
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
501
 
            direction, delayed_graph_generation)
 
577
        try:
 
578
            # If we only want to see linear revisions, we can iterate ...
 
579
            iter_revs = _linear_view_revisions(
 
580
                branch, start_rev_id, end_rev_id,
 
581
                exclude_common_ancestry=exclude_common_ancestry)
 
582
            # If a start limit was given and it's not obviously an
 
583
            # ancestor of the end limit, check it before outputting anything
 
584
            if (direction == 'forward'
 
585
                or (start_rev_id and not _is_obvious_ancestor(
 
586
                        branch, start_rev_id, end_rev_id))):
 
587
                    iter_revs = list(iter_revs)
 
588
            if direction == 'forward':
 
589
                iter_revs = reversed(iter_revs)
 
590
            return iter_revs
 
591
        except _StartNotLinearAncestor:
 
592
            # Switch to the slower implementation that may be able to find a
 
593
            # non-obvious ancestor out of the left-hand history.
 
594
            pass
 
595
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
596
                                        direction, delayed_graph_generation,
 
597
                                        exclude_common_ancestry)
 
598
    if direction == 'forward':
 
599
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
600
    return iter_revs
502
601
 
503
602
 
504
603
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
506
605
        # It's the tip
507
606
        return [(br_rev_id, br_revno, 0)]
508
607
    else:
509
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
510
 
        revno_str = '.'.join(str(n) for n in revno)
 
608
        revno_str = _compute_revno_str(branch, rev_id)
511
609
        return [(rev_id, revno_str, 0)]
512
610
 
513
611
 
514
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
515
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
516
 
    # If a start limit was given and it's not obviously an
517
 
    # ancestor of the end limit, check it before outputting anything
518
 
    if direction == 'forward' or (start_rev_id
519
 
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
520
 
        try:
521
 
            result = list(result)
522
 
        except _StartNotLinearAncestor:
523
 
            raise errors.BzrCommandError('Start revision not found in'
524
 
                ' left-hand history of end revision.')
525
 
    if direction == 'forward':
526
 
        result = reversed(result)
527
 
    return result
528
 
 
529
 
 
530
612
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
531
 
    delayed_graph_generation):
 
613
                            delayed_graph_generation,
 
614
                            exclude_common_ancestry=False):
532
615
    # On large trees, generating the merge graph can take 30-60 seconds
533
616
    # so we delay doing it until a merge is detected, incrementally
534
617
    # returning initial (non-merge) revisions while we can.
 
618
 
 
619
    # The above is only true for old formats (<= 0.92), for newer formats, a
 
620
    # couple of seconds only should be needed to load the whole graph and the
 
621
    # other graph operations needed are even faster than that -- vila 100201
535
622
    initial_revisions = []
536
623
    if delayed_graph_generation:
537
624
        try:
538
 
            for rev_id, revno, depth in \
539
 
                _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
625
            for rev_id, revno, depth in  _linear_view_revisions(
 
626
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
540
627
                if _has_merges(branch, rev_id):
 
628
                    # The end_rev_id can be nested down somewhere. We need an
 
629
                    # explicit ancestry check. There is an ambiguity here as we
 
630
                    # may not raise _StartNotLinearAncestor for a revision that
 
631
                    # is an ancestor but not a *linear* one. But since we have
 
632
                    # loaded the graph to do the check (or calculate a dotted
 
633
                    # revno), we may as well accept to show the log...  We need
 
634
                    # the check only if start_rev_id is not None as all
 
635
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
 
636
                    # -- vila 20100319
 
637
                    graph = branch.repository.get_graph()
 
638
                    if (start_rev_id is not None
 
639
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
640
                        raise _StartNotLinearAncestor()
 
641
                    # Since we collected the revisions so far, we need to
 
642
                    # adjust end_rev_id.
541
643
                    end_rev_id = rev_id
542
644
                    break
543
645
                else:
544
646
                    initial_revisions.append((rev_id, revno, depth))
545
647
            else:
546
648
                # No merged revisions found
547
 
                if direction == 'reverse':
548
 
                    return initial_revisions
549
 
                elif direction == 'forward':
550
 
                    return reversed(initial_revisions)
551
 
                else:
552
 
                    raise ValueError('invalid direction %r' % direction)
 
649
                return initial_revisions
553
650
        except _StartNotLinearAncestor:
554
651
            # A merge was never detected so the lower revision limit can't
555
652
            # be nested down somewhere
556
 
            raise errors.BzrCommandError('Start revision not found in'
557
 
                ' history of end revision.')
 
653
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
654
                ' history of end revision.'))
 
655
 
 
656
    # We exit the loop above because we encounter a revision with merges, from
 
657
    # this revision, we need to switch to _graph_view_revisions.
558
658
 
559
659
    # A log including nested merges is required. If the direction is reverse,
560
660
    # we rebase the initial merge depths so that the development line is
563
663
    # indented at the end seems slightly nicer in that case.
564
664
    view_revisions = chain(iter(initial_revisions),
565
665
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
566
 
        rebase_initial_depths=direction == 'reverse'))
567
 
    if direction == 'reverse':
568
 
        return view_revisions
569
 
    elif direction == 'forward':
570
 
        # Forward means oldest first, adjusting for depth.
571
 
        view_revisions = reverse_by_depth(list(view_revisions))
572
 
        return _rebase_merge_depth(view_revisions)
573
 
    else:
574
 
        raise ValueError('invalid direction %r' % direction)
 
666
                              rebase_initial_depths=(direction == 'reverse'),
 
667
                              exclude_common_ancestry=exclude_common_ancestry))
 
668
    return view_revisions
575
669
 
576
670
 
577
671
def _has_merges(branch, rev_id):
580
674
    return len(parents) > 1
581
675
 
582
676
 
 
677
def _compute_revno_str(branch, rev_id):
 
678
    """Compute the revno string from a rev_id.
 
679
 
 
680
    :return: The revno string, or None if the revision is not in the supplied
 
681
        branch.
 
682
    """
 
683
    try:
 
684
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
685
    except errors.NoSuchRevision:
 
686
        # The revision must be outside of this branch
 
687
        return None
 
688
    else:
 
689
        return '.'.join(str(n) for n in revno)
 
690
 
 
691
 
583
692
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
584
693
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
585
694
    if start_rev_id and end_rev_id:
586
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
587
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
695
        try:
 
696
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
697
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
698
        except errors.NoSuchRevision:
 
699
            # one or both is not in the branch; not obvious
 
700
            return False
588
701
        if len(start_dotted) == 1 and len(end_dotted) == 1:
589
702
            # both on mainline
590
703
            return start_dotted[0] <= end_dotted[0]
595
708
        else:
596
709
            # not obvious
597
710
            return False
 
711
    # if either start or end is not specified then we use either the first or
 
712
    # the last revision and *they* are obvious ancestors.
598
713
    return True
599
714
 
600
715
 
601
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
716
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
717
                           exclude_common_ancestry=False):
602
718
    """Calculate a sequence of revisions to view, newest to oldest.
603
719
 
604
720
    :param start_rev_id: the lower revision-id
605
721
    :param end_rev_id: the upper revision-id
 
722
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
723
        the iterated revisions.
606
724
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
607
725
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
608
 
      is not found walking the left-hand history
 
726
        is not found walking the left-hand history
609
727
    """
610
728
    br_revno, br_rev_id = branch.last_revision_info()
611
729
    repo = branch.repository
 
730
    graph = repo.get_graph()
612
731
    if start_rev_id is None and end_rev_id is None:
613
732
        cur_revno = br_revno
614
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
733
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
734
            (_mod_revision.NULL_REVISION,)):
615
735
            yield revision_id, str(cur_revno), 0
616
736
            cur_revno -= 1
617
737
    else:
618
738
        if end_rev_id is None:
619
739
            end_rev_id = br_rev_id
620
740
        found_start = start_rev_id is None
621
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
622
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
623
 
            revno_str = '.'.join(str(n) for n in revno)
 
741
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
742
                (_mod_revision.NULL_REVISION,)):
 
743
            revno_str = _compute_revno_str(branch, revision_id)
624
744
            if not found_start and revision_id == start_rev_id:
625
 
                yield revision_id, revno_str, 0
 
745
                if not exclude_common_ancestry:
 
746
                    yield revision_id, revno_str, 0
626
747
                found_start = True
627
748
                break
628
749
            else:
633
754
 
634
755
 
635
756
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
636
 
    rebase_initial_depths=True):
 
757
                          rebase_initial_depths=True,
 
758
                          exclude_common_ancestry=False):
637
759
    """Calculate revisions to view including merges, newest to oldest.
638
760
 
639
761
    :param branch: the branch
643
765
      revision is found?
644
766
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
645
767
    """
 
768
    if exclude_common_ancestry:
 
769
        stop_rule = 'with-merges-without-common-ancestry'
 
770
    else:
 
771
        stop_rule = 'with-merges'
646
772
    view_revisions = branch.iter_merge_sorted_revisions(
647
773
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
648
 
        stop_rule="with-merges")
 
774
        stop_rule=stop_rule)
649
775
    if not rebase_initial_depths:
650
776
        for (rev_id, merge_depth, revno, end_of_merge
651
777
             ) in view_revisions:
662
788
                depth_adjustment = merge_depth
663
789
            if depth_adjustment:
664
790
                if merge_depth < depth_adjustment:
 
791
                    # From now on we reduce the depth adjustement, this can be
 
792
                    # surprising for users. The alternative requires two passes
 
793
                    # which breaks the fast display of the first revision
 
794
                    # though.
665
795
                    depth_adjustment = merge_depth
666
796
                merge_depth -= depth_adjustment
667
797
            yield rev_id, '.'.join(map(str, revno)), merge_depth
668
798
 
669
799
 
670
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
671
 
        specific_fileid, generate_merge_revisions):
672
 
    """Calculate the revisions to view.
673
 
 
674
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
675
 
             a list of the same tuples.
676
 
    """
677
 
    # This method is no longer called by the main code path.
678
 
    # It is retained for API compatibility and may be deprecated
679
 
    # soon. IGC 20090116
680
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
681
 
        end_revision)
682
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
683
 
        direction, generate_merge_revisions or specific_fileid))
684
 
    if specific_fileid:
685
 
        view_revisions = _filter_revisions_touching_file_id(branch,
686
 
            specific_fileid, view_revisions,
687
 
            include_merges=generate_merge_revisions)
688
 
    return _rebase_merge_depth(view_revisions)
689
 
 
690
 
 
691
800
def _rebase_merge_depth(view_revisions):
692
801
    """Adjust depths upwards so the top level is 0."""
693
802
    # If either the first or last revision have a merge_depth of 0, we're done
737
846
    return log_rev_iterator
738
847
 
739
848
 
740
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
849
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
741
850
    """Create a filtered iterator of log_rev_iterator matching on a regex.
742
851
 
743
852
    :param branch: The branch being logged.
744
853
    :param generate_delta: Whether to generate a delta for each revision.
745
 
    :param search: A user text search string.
 
854
    :param match: A dictionary with properties as keys and lists of strings
 
855
        as values. To match, a revision may match any of the supplied strings
 
856
        within a single property but must match at least one string for each
 
857
        property.
746
858
    :param log_rev_iterator: An input iterator containing all revisions that
747
859
        could be displayed, in lists.
748
860
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
749
861
        delta).
750
862
    """
751
 
    if search is None:
 
863
    if match is None:
752
864
        return log_rev_iterator
753
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
754
 
            'log message filter')
755
 
    return _filter_message_re(searchRE, log_rev_iterator)
756
 
 
757
 
 
758
 
def _filter_message_re(searchRE, log_rev_iterator):
 
865
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
866
                for (k,v) in match.iteritems()]
 
867
    return _filter_re(searchRE, log_rev_iterator)
 
868
 
 
869
 
 
870
def _filter_re(searchRE, log_rev_iterator):
759
871
    for revs in log_rev_iterator:
760
 
        new_revs = []
761
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
762
 
            if searchRE.search(rev.message):
763
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
764
 
        yield new_revs
765
 
 
 
872
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
873
        if new_revs:
 
874
            yield new_revs
 
875
 
 
876
def _match_filter(searchRE, rev):
 
877
    strings = {
 
878
               'message': (rev.message,),
 
879
               'committer': (rev.committer,),
 
880
               'author': (rev.get_apparent_authors()),
 
881
               'bugs': list(rev.iter_bugs())
 
882
               }
 
883
    strings[''] = [item for inner_list in strings.itervalues()
 
884
                   for item in inner_list]
 
885
    for (k,v) in searchRE:
 
886
        if k in strings and not _match_any_filter(strings[k], v):
 
887
            return False
 
888
    return True
 
889
 
 
890
def _match_any_filter(strings, res):
 
891
    return any([filter(None, map(re.search, strings)) for re in res])
766
892
 
767
893
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
768
894
    fileids=None, direction='reverse'):
841
967
 
842
968
def _update_fileids(delta, fileids, stop_on):
843
969
    """Update the set of file-ids to search based on file lifecycle events.
844
 
    
 
970
 
845
971
    :param fileids: a set of fileids to update
846
972
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
847
973
      fileids set once their add or remove entry is detected respectively
888
1014
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
889
1015
        delta).
890
1016
    """
891
 
    repository = branch.repository
892
1017
    num = 9
893
1018
    for batch in log_rev_iterator:
894
1019
        batch = iter(batch)
943
1068
    if branch_revno != 0:
944
1069
        if (start_rev_id == _mod_revision.NULL_REVISION
945
1070
            or end_rev_id == _mod_revision.NULL_REVISION):
946
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1071
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
947
1072
        if start_revno > end_revno:
948
 
            raise errors.BzrCommandError("Start revision must be older than "
949
 
                                         "the end revision.")
 
1073
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1074
                                         "older than the end revision."))
950
1075
    return (start_rev_id, end_rev_id)
951
1076
 
952
1077
 
1001
1126
 
1002
1127
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1003
1128
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1004
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1129
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1005
1130
    if start_revno > end_revno:
1006
 
        raise errors.BzrCommandError("Start revision must be older than "
1007
 
                                     "the end revision.")
 
1131
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1132
                                     "than the end revision."))
1008
1133
 
1009
1134
    if end_revno < start_revno:
1010
1135
        return None, None, None, None
1011
1136
    cur_revno = branch_revno
1012
1137
    rev_nos = {}
1013
1138
    mainline_revs = []
1014
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1015
 
                        branch_last_revision):
 
1139
    graph = branch.repository.get_graph()
 
1140
    for revision_id in graph.iter_lefthand_ancestry(
 
1141
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1016
1142
        if cur_revno < start_revno:
1017
1143
            # We have gone far enough, but we always add 1 more revision
1018
1144
            rev_nos[revision_id] = cur_revno
1032
1158
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1033
1159
 
1034
1160
 
1035
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1036
 
    """Filter view_revisions based on revision ranges.
1037
 
 
1038
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1039
 
            tuples to be filtered.
1040
 
 
1041
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1042
 
            If NONE then all revisions up to the end_rev_id are logged.
1043
 
 
1044
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1045
 
            If NONE then all revisions up to the end of the log are logged.
1046
 
 
1047
 
    :return: The filtered view_revisions.
1048
 
    """
1049
 
    # This method is no longer called by the main code path.
1050
 
    # It may be removed soon. IGC 20090127
1051
 
    if start_rev_id or end_rev_id:
1052
 
        revision_ids = [r for r, n, d in view_revisions]
1053
 
        if start_rev_id:
1054
 
            start_index = revision_ids.index(start_rev_id)
1055
 
        else:
1056
 
            start_index = 0
1057
 
        if start_rev_id == end_rev_id:
1058
 
            end_index = start_index
1059
 
        else:
1060
 
            if end_rev_id:
1061
 
                end_index = revision_ids.index(end_rev_id)
1062
 
            else:
1063
 
                end_index = len(view_revisions) - 1
1064
 
        # To include the revisions merged into the last revision,
1065
 
        # extend end_rev_id down to, but not including, the next rev
1066
 
        # with the same or lesser merge_depth
1067
 
        end_merge_depth = view_revisions[end_index][2]
1068
 
        try:
1069
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1070
 
                if view_revisions[index][2] <= end_merge_depth:
1071
 
                    end_index = index - 1
1072
 
                    break
1073
 
        except IndexError:
1074
 
            # if the search falls off the end then log to the end as well
1075
 
            end_index = len(view_revisions) - 1
1076
 
        view_revisions = view_revisions[start_index:end_index+1]
1077
 
    return view_revisions
1078
 
 
1079
 
 
1080
1161
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1081
1162
    include_merges=True):
1082
1163
    r"""Return the list of revision ids which touch a given file id.
1085
1166
    This includes the revisions which directly change the file id,
1086
1167
    and the revisions which merge these changes. So if the
1087
1168
    revision graph is::
 
1169
 
1088
1170
        A-.
1089
1171
        |\ \
1090
1172
        B C E
1117
1199
    """
1118
1200
    # Lookup all possible text keys to determine which ones actually modified
1119
1201
    # the file.
 
1202
    graph = branch.repository.get_file_graph()
 
1203
    get_parent_map = graph.get_parent_map
1120
1204
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1121
1205
    next_keys = None
1122
1206
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1126
1210
    #       indexing layer. We might consider passing in hints as to the known
1127
1211
    #       access pattern (sparse/clustered, high success rate/low success
1128
1212
    #       rate). This particular access is clustered with a low success rate.
1129
 
    get_parent_map = branch.repository.texts.get_parent_map
1130
1213
    modified_text_revisions = set()
1131
1214
    chunk_size = 1000
1132
1215
    for start in xrange(0, len(text_keys), chunk_size):
1159
1242
    return result
1160
1243
 
1161
1244
 
1162
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1163
 
                       include_merges=True):
1164
 
    """Produce an iterator of revisions to show
1165
 
    :return: an iterator of (revision_id, revno, merge_depth)
1166
 
    (if there is no revno for a revision, None is supplied)
1167
 
    """
1168
 
    # This method is no longer called by the main code path.
1169
 
    # It is retained for API compatibility and may be deprecated
1170
 
    # soon. IGC 20090127
1171
 
    if not include_merges:
1172
 
        revision_ids = mainline_revs[1:]
1173
 
        if direction == 'reverse':
1174
 
            revision_ids.reverse()
1175
 
        for revision_id in revision_ids:
1176
 
            yield revision_id, str(rev_nos[revision_id]), 0
1177
 
        return
1178
 
    graph = branch.repository.get_graph()
1179
 
    # This asks for all mainline revisions, which means we only have to spider
1180
 
    # sideways, rather than depth history. That said, its still size-of-history
1181
 
    # and should be addressed.
1182
 
    # mainline_revisions always includes an extra revision at the beginning, so
1183
 
    # don't request it.
1184
 
    parent_map = dict(((key, value) for key, value in
1185
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1186
 
    # filter out ghosts; merge_sort errors on ghosts.
1187
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1188
 
    merge_sorted_revisions = tsort.merge_sort(
1189
 
        rev_graph,
1190
 
        mainline_revs[-1],
1191
 
        mainline_revs,
1192
 
        generate_revno=True)
1193
 
 
1194
 
    if direction == 'forward':
1195
 
        # forward means oldest first.
1196
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1197
 
    elif direction != 'reverse':
1198
 
        raise ValueError('invalid direction %r' % direction)
1199
 
 
1200
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1201
 
         ) in merge_sorted_revisions:
1202
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1203
 
 
1204
 
 
1205
1245
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1206
1246
    """Reverse revisions by depth.
1207
1247
 
1242
1282
    """
1243
1283
 
1244
1284
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1245
 
                 tags=None, diff=None):
 
1285
                 tags=None, diff=None, signature=None):
1246
1286
        self.rev = rev
1247
 
        self.revno = str(revno)
 
1287
        if revno is None:
 
1288
            self.revno = None
 
1289
        else:
 
1290
            self.revno = str(revno)
1248
1291
        self.merge_depth = merge_depth
1249
1292
        self.delta = delta
1250
1293
        self.tags = tags
1251
1294
        self.diff = diff
 
1295
        self.signature = signature
1252
1296
 
1253
1297
 
1254
1298
class LogFormatter(object):
1263
1307
    to indicate which LogRevision attributes it supports:
1264
1308
 
1265
1309
    - supports_delta must be True if this log formatter supports delta.
1266
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1267
 
        attribute describes whether the 'short_status' format (1) or the long
1268
 
        one (2) should be used.
 
1310
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1311
      attribute describes whether the 'short_status' format (1) or the long
 
1312
      one (2) should be used.
1269
1313
 
1270
1314
    - supports_merge_revisions must be True if this log formatter supports
1271
 
        merge revisions.  If not, then only mainline revisions will be passed
1272
 
        to the formatter.
 
1315
      merge revisions.  If not, then only mainline revisions will be passed
 
1316
      to the formatter.
1273
1317
 
1274
1318
    - preferred_levels is the number of levels this formatter defaults to.
1275
 
        The default value is zero meaning display all levels.
1276
 
        This value is only relevant if supports_merge_revisions is True.
 
1319
      The default value is zero meaning display all levels.
 
1320
      This value is only relevant if supports_merge_revisions is True.
1277
1321
 
1278
1322
    - supports_tags must be True if this log formatter supports tags.
1279
 
        Otherwise the tags attribute may not be populated.
 
1323
      Otherwise the tags attribute may not be populated.
1280
1324
 
1281
1325
    - supports_diff must be True if this log formatter supports diffs.
1282
 
        Otherwise the diff attribute may not be populated.
 
1326
      Otherwise the diff attribute may not be populated.
 
1327
 
 
1328
    - supports_signatures must be True if this log formatter supports GPG
 
1329
      signatures.
1283
1330
 
1284
1331
    Plugins can register functions to show custom revision properties using
1285
1332
    the properties_handler_registry. The registered function
1286
 
    must respect the following interface description:
 
1333
    must respect the following interface description::
 
1334
 
1287
1335
        def my_show_properties(properties_dict):
1288
1336
            # code that returns a dict {'name':'value'} of the properties
1289
1337
            # to be shown
1291
1339
    preferred_levels = 0
1292
1340
 
1293
1341
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1294
 
                 delta_format=None, levels=None, show_advice=False):
 
1342
                 delta_format=None, levels=None, show_advice=False,
 
1343
                 to_exact_file=None, author_list_handler=None):
1295
1344
        """Create a LogFormatter.
1296
1345
 
1297
1346
        :param to_file: the file to output to
 
1347
        :param to_exact_file: if set, gives an output stream to which
 
1348
             non-Unicode diffs are written.
1298
1349
        :param show_ids: if True, revision-ids are to be displayed
1299
1350
        :param show_timezone: the timezone to use
1300
1351
        :param delta_format: the level of delta information to display
1303
1354
          let the log formatter decide.
1304
1355
        :param show_advice: whether to show advice at the end of the
1305
1356
          log or not
 
1357
        :param author_list_handler: callable generating a list of
 
1358
          authors to display for a given revision
1306
1359
        """
1307
1360
        self.to_file = to_file
1308
1361
        # 'exact' stream used to show diff, it should print content 'as is'
1309
1362
        # and should not try to decode/encode it to unicode to avoid bug #328007
1310
 
        self.to_exact_file = getattr(to_file, 'stream', to_file)
 
1363
        if to_exact_file is not None:
 
1364
            self.to_exact_file = to_exact_file
 
1365
        else:
 
1366
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1367
            # for code that expects to get diffs to pass in the exact file
 
1368
            # stream
 
1369
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1311
1370
        self.show_ids = show_ids
1312
1371
        self.show_timezone = show_timezone
1313
1372
        if delta_format is None:
1317
1376
        self.levels = levels
1318
1377
        self._show_advice = show_advice
1319
1378
        self._merge_count = 0
 
1379
        self._author_list_handler = author_list_handler
1320
1380
 
1321
1381
    def get_levels(self):
1322
1382
        """Get the number of levels to display or 0 for all."""
1341
1401
            if advice_sep:
1342
1402
                self.to_file.write(advice_sep)
1343
1403
            self.to_file.write(
1344
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1404
                "Use --include-merged or -n0 to see merged revisions.\n")
1345
1405
 
1346
1406
    def get_advice_separator(self):
1347
1407
        """Get the text separating the log from the closing advice."""
1354
1414
        return address
1355
1415
 
1356
1416
    def short_author(self, rev):
1357
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1358
 
        if name:
1359
 
            return name
1360
 
        return address
 
1417
        return self.authors(rev, 'first', short=True, sep=', ')
 
1418
 
 
1419
    def authors(self, rev, who, short=False, sep=None):
 
1420
        """Generate list of authors, taking --authors option into account.
 
1421
 
 
1422
        The caller has to specify the name of a author list handler,
 
1423
        as provided by the author list registry, using the ``who``
 
1424
        argument.  That name only sets a default, though: when the
 
1425
        user selected a different author list generation using the
 
1426
        ``--authors`` command line switch, as represented by the
 
1427
        ``author_list_handler`` constructor argument, that value takes
 
1428
        precedence.
 
1429
 
 
1430
        :param rev: The revision for which to generate the list of authors.
 
1431
        :param who: Name of the default handler.
 
1432
        :param short: Whether to shorten names to either name or address.
 
1433
        :param sep: What separator to use for automatic concatenation.
 
1434
        """
 
1435
        if self._author_list_handler is not None:
 
1436
            # The user did specify --authors, which overrides the default
 
1437
            author_list_handler = self._author_list_handler
 
1438
        else:
 
1439
            # The user didn't specify --authors, so we use the caller's default
 
1440
            author_list_handler = author_list_registry.get(who)
 
1441
        names = author_list_handler(rev)
 
1442
        if short:
 
1443
            for i in range(len(names)):
 
1444
                name, address = config.parse_username(names[i])
 
1445
                if name:
 
1446
                    names[i] = name
 
1447
                else:
 
1448
                    names[i] = address
 
1449
        if sep is not None:
 
1450
            names = sep.join(names)
 
1451
        return names
1361
1452
 
1362
1453
    def merge_marker(self, revision):
1363
1454
        """Get the merge marker to include in the output or '' if none."""
1367
1458
        else:
1368
1459
            return ''
1369
1460
 
1370
 
    def show_foreign_info(self, rev, indent):
 
1461
    def show_properties(self, revision, indent):
 
1462
        """Displays the custom properties returned by each registered handler.
 
1463
 
 
1464
        If a registered handler raises an error it is propagated.
 
1465
        """
 
1466
        for line in self.custom_properties(revision):
 
1467
            self.to_file.write("%s%s\n" % (indent, line))
 
1468
 
 
1469
    def custom_properties(self, revision):
 
1470
        """Format the custom properties returned by each registered handler.
 
1471
 
 
1472
        If a registered handler raises an error it is propagated.
 
1473
 
 
1474
        :return: a list of formatted lines (excluding trailing newlines)
 
1475
        """
 
1476
        lines = self._foreign_info_properties(revision)
 
1477
        for key, handler in properties_handler_registry.iteritems():
 
1478
            lines.extend(self._format_properties(handler(revision)))
 
1479
        return lines
 
1480
 
 
1481
    def _foreign_info_properties(self, rev):
1371
1482
        """Custom log displayer for foreign revision identifiers.
1372
1483
 
1373
1484
        :param rev: Revision object.
1374
1485
        """
1375
1486
        # Revision comes directly from a foreign repository
1376
1487
        if isinstance(rev, foreign.ForeignRevision):
1377
 
            self._write_properties(indent, rev.mapping.vcs.show_foreign_revid(
1378
 
                rev.foreign_revid))
1379
 
            return
 
1488
            return self._format_properties(
 
1489
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1380
1490
 
1381
1491
        # Imported foreign revision revision ids always contain :
1382
1492
        if not ":" in rev.revision_id:
1383
 
            return
 
1493
            return []
1384
1494
 
1385
1495
        # Revision was once imported from a foreign repository
1386
1496
        try:
1387
1497
            foreign_revid, mapping = \
1388
1498
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
1389
1499
        except errors.InvalidRevisionId:
1390
 
            return
 
1500
            return []
1391
1501
 
1392
 
        self._write_properties(indent, 
 
1502
        return self._format_properties(
1393
1503
            mapping.vcs.show_foreign_revid(foreign_revid))
1394
1504
 
1395
 
    def show_properties(self, revision, indent):
1396
 
        """Displays the custom properties returned by each registered handler.
1397
 
 
1398
 
        If a registered handler raises an error it is propagated.
1399
 
        """
1400
 
        for key, handler in properties_handler_registry.iteritems():
1401
 
            self._write_properties(indent, handler(revision))
1402
 
 
1403
 
    def _write_properties(self, indent, properties):
 
1505
    def _format_properties(self, properties):
 
1506
        lines = []
1404
1507
        for key, value in properties.items():
1405
 
            self.to_file.write(indent + key + ': ' + value + '\n')
 
1508
            lines.append(key + ': ' + value)
 
1509
        return lines
1406
1510
 
1407
1511
    def show_diff(self, to_file, diff, indent):
1408
1512
        for l in diff.rstrip().split('\n'):
1409
1513
            to_file.write(indent + '%s\n' % (l,))
1410
1514
 
1411
1515
 
 
1516
# Separator between revisions in long format
 
1517
_LONG_SEP = '-' * 60
 
1518
 
 
1519
 
1412
1520
class LongLogFormatter(LogFormatter):
1413
1521
 
1414
1522
    supports_merge_revisions = True
1416
1524
    supports_delta = True
1417
1525
    supports_tags = True
1418
1526
    supports_diff = True
 
1527
    supports_signatures = True
 
1528
 
 
1529
    def __init__(self, *args, **kwargs):
 
1530
        super(LongLogFormatter, self).__init__(*args, **kwargs)
 
1531
        if self.show_timezone == 'original':
 
1532
            self.date_string = self._date_string_original_timezone
 
1533
        else:
 
1534
            self.date_string = self._date_string_with_timezone
 
1535
 
 
1536
    def _date_string_with_timezone(self, rev):
 
1537
        return format_date(rev.timestamp, rev.timezone or 0,
 
1538
                           self.show_timezone)
 
1539
 
 
1540
    def _date_string_original_timezone(self, rev):
 
1541
        return format_date_with_offset_in_original_timezone(rev.timestamp,
 
1542
            rev.timezone or 0)
1419
1543
 
1420
1544
    def log_revision(self, revision):
1421
1545
        """Log a revision, either merged or not."""
1422
1546
        indent = '    ' * revision.merge_depth
1423
 
        to_file = self.to_file
1424
 
        to_file.write(indent + '-' * 60 + '\n')
 
1547
        lines = [_LONG_SEP]
1425
1548
        if revision.revno is not None:
1426
 
            to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
 
1549
            lines.append('revno: %s%s' % (revision.revno,
1427
1550
                self.merge_marker(revision)))
1428
1551
        if revision.tags:
1429
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
 
1552
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1553
        if self.show_ids or revision.revno is None:
 
1554
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1430
1555
        if self.show_ids:
1431
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1432
 
            to_file.write('\n')
1433
1556
            for parent_id in revision.rev.parent_ids:
1434
 
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
1435
 
        self.show_foreign_info(revision.rev, indent)
1436
 
        self.show_properties(revision.rev, indent)
 
1557
                lines.append('parent: %s' % (parent_id,))
 
1558
        lines.extend(self.custom_properties(revision.rev))
1437
1559
 
1438
1560
        committer = revision.rev.committer
1439
 
        authors = revision.rev.get_apparent_authors()
 
1561
        authors = self.authors(revision.rev, 'all')
1440
1562
        if authors != [committer]:
1441
 
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1442
 
        to_file.write(indent + 'committer: %s\n' % (committer,))
 
1563
            lines.append('author: %s' % (", ".join(authors),))
 
1564
        lines.append('committer: %s' % (committer,))
1443
1565
 
1444
1566
        branch_nick = revision.rev.properties.get('branch-nick', None)
1445
1567
        if branch_nick is not None:
1446
 
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
1447
 
 
1448
 
        date_str = format_date(revision.rev.timestamp,
1449
 
                               revision.rev.timezone or 0,
1450
 
                               self.show_timezone)
1451
 
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
1452
 
 
1453
 
        to_file.write(indent + 'message:\n')
 
1568
            lines.append('branch nick: %s' % (branch_nick,))
 
1569
 
 
1570
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1571
 
 
1572
        if revision.signature is not None:
 
1573
            lines.append('signature: ' + revision.signature)
 
1574
 
 
1575
        lines.append('message:')
1454
1576
        if not revision.rev.message:
1455
 
            to_file.write(indent + '  (no message)\n')
 
1577
            lines.append('  (no message)')
1456
1578
        else:
1457
1579
            message = revision.rev.message.rstrip('\r\n')
1458
1580
            for l in message.split('\n'):
1459
 
                to_file.write(indent + '  %s\n' % (l,))
 
1581
                lines.append('  %s' % (l,))
 
1582
 
 
1583
        # Dump the output, appending the delta and diff if requested
 
1584
        to_file = self.to_file
 
1585
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1460
1586
        if revision.delta is not None:
1461
 
            # We don't respect delta_format for compatibility
1462
 
            revision.delta.show(to_file, self.show_ids, indent=indent,
1463
 
                                short_status=False)
 
1587
            # Use the standard status output to display changes
 
1588
            from bzrlib.delta import report_delta
 
1589
            report_delta(to_file, revision.delta, short_status=False,
 
1590
                         show_ids=self.show_ids, indent=indent)
1464
1591
        if revision.diff is not None:
1465
1592
            to_file.write(indent + 'diff:\n')
 
1593
            to_file.flush()
1466
1594
            # Note: we explicitly don't indent the diff (relative to the
1467
1595
            # revision information) so that the output can be fed to patch -p0
1468
1596
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1597
            self.to_exact_file.flush()
1469
1598
 
1470
1599
    def get_advice_separator(self):
1471
1600
        """Get the text separating the log from the closing advice."""
1495
1624
        indent = '    ' * depth
1496
1625
        revno_width = self.revno_width_by_depth.get(depth)
1497
1626
        if revno_width is None:
1498
 
            if revision.revno.find('.') == -1:
 
1627
            if revision.revno is None or revision.revno.find('.') == -1:
1499
1628
                # mainline revno, e.g. 12345
1500
1629
                revno_width = 5
1501
1630
            else:
1509
1638
        if revision.tags:
1510
1639
            tags = ' {%s}' % (', '.join(revision.tags))
1511
1640
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1512
 
                revision.revno, self.short_author(revision.rev),
 
1641
                revision.revno or "", self.short_author(revision.rev),
1513
1642
                format_date(revision.rev.timestamp,
1514
1643
                            revision.rev.timezone or 0,
1515
1644
                            self.show_timezone, date_fmt="%Y-%m-%d",
1516
1645
                            show_offset=False),
1517
1646
                tags, self.merge_marker(revision)))
1518
 
        self.show_foreign_info(revision.rev, indent+offset)
1519
1647
        self.show_properties(revision.rev, indent+offset)
1520
 
        if self.show_ids:
 
1648
        if self.show_ids or revision.revno is None:
1521
1649
            to_file.write(indent + offset + 'revision-id:%s\n'
1522
1650
                          % (revision.rev.revision_id,))
1523
1651
        if not revision.rev.message:
1528
1656
                to_file.write(indent + offset + '%s\n' % (l,))
1529
1657
 
1530
1658
        if revision.delta is not None:
1531
 
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
1532
 
                                short_status=self.delta_format==1)
 
1659
            # Use the standard status output to display changes
 
1660
            from bzrlib.delta import report_delta
 
1661
            report_delta(to_file, revision.delta,
 
1662
                         short_status=self.delta_format==1,
 
1663
                         show_ids=self.show_ids, indent=indent + offset)
1533
1664
        if revision.diff is not None:
1534
1665
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1535
1666
        to_file.write('\n')
1543
1674
 
1544
1675
    def __init__(self, *args, **kwargs):
1545
1676
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1546
 
        self._max_chars = terminal_width() - 1
 
1677
        width = terminal_width()
 
1678
        if width is not None:
 
1679
            # we need one extra space for terminals that wrap on last char
 
1680
            width = width - 1
 
1681
        self._max_chars = width
1547
1682
 
1548
1683
    def truncate(self, str, max_len):
1549
 
        if len(str) <= max_len:
 
1684
        if max_len is None or len(str) <= max_len:
1550
1685
            return str
1551
 
        return str[:max_len-3]+'...'
 
1686
        return str[:max_len-3] + '...'
1552
1687
 
1553
1688
    def date_string(self, rev):
1554
1689
        return format_date(rev.timestamp, rev.timezone or 0,
1569
1704
 
1570
1705
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1571
1706
        """Format log info into one string. Truncate tail of string
1572
 
        :param  revno:      revision number or None.
1573
 
                            Revision numbers counts from 1.
1574
 
        :param  rev:        revision object
1575
 
        :param  max_chars:  maximum length of resulting string
1576
 
        :param  tags:       list of tags or None
1577
 
        :param  prefix:     string to prefix each line
1578
 
        :return:            formatted truncated string
 
1707
 
 
1708
        :param revno:      revision number or None.
 
1709
                           Revision numbers counts from 1.
 
1710
        :param rev:        revision object
 
1711
        :param max_chars:  maximum length of resulting string
 
1712
        :param tags:       list of tags or None
 
1713
        :param prefix:     string to prefix each line
 
1714
        :return:           formatted truncated string
1579
1715
        """
1580
1716
        out = []
1581
1717
        if revno:
1582
1718
            # show revno only when is not None
1583
1719
            out.append("%s:" % revno)
1584
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1720
        if max_chars is not None:
 
1721
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1722
        else:
 
1723
            out.append(self.short_author(rev))
1585
1724
        out.append(self.date_string(rev))
1586
1725
        if len(rev.parent_ids) > 1:
1587
1726
            out.append('[merge]')
1606
1745
                               self.show_timezone,
1607
1746
                               date_fmt='%Y-%m-%d',
1608
1747
                               show_offset=False)
1609
 
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1748
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1749
        committer_str = committer_str.replace(' <', '  <')
1610
1750
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1611
1751
 
1612
1752
        if revision.delta is not None and revision.delta.has_changed():
1645
1785
        return self.get(name)(*args, **kwargs)
1646
1786
 
1647
1787
    def get_default(self, branch):
1648
 
        return self.get(branch.get_config().log_format())
 
1788
        c = branch.get_config_stack()
 
1789
        return self.get(c.get('log_format'))
1649
1790
 
1650
1791
 
1651
1792
log_formatter_registry = LogFormatterRegistry()
1652
1793
 
1653
1794
 
1654
1795
log_formatter_registry.register('short', ShortLogFormatter,
1655
 
                                'Moderately short log format')
 
1796
                                'Moderately short log format.')
1656
1797
log_formatter_registry.register('long', LongLogFormatter,
1657
 
                                'Detailed log format')
 
1798
                                'Detailed log format.')
1658
1799
log_formatter_registry.register('line', LineLogFormatter,
1659
 
                                'Log format with one line per revision')
 
1800
                                'Log format with one line per revision.')
1660
1801
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1661
 
                                'Format used by GNU ChangeLog files')
 
1802
                                'Format used by GNU ChangeLog files.')
1662
1803
 
1663
1804
 
1664
1805
def register_formatter(name, formatter):
1674
1815
    try:
1675
1816
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1676
1817
    except KeyError:
1677
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1678
 
 
1679
 
 
1680
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1681
 
    # deprecated; for compatibility
1682
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1683
 
    lf.show(revno, rev, delta)
 
1818
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1819
 
 
1820
 
 
1821
def author_list_all(rev):
 
1822
    return rev.get_apparent_authors()[:]
 
1823
 
 
1824
 
 
1825
def author_list_first(rev):
 
1826
    lst = rev.get_apparent_authors()
 
1827
    try:
 
1828
        return [lst[0]]
 
1829
    except IndexError:
 
1830
        return []
 
1831
 
 
1832
 
 
1833
def author_list_committer(rev):
 
1834
    return [rev.committer]
 
1835
 
 
1836
 
 
1837
author_list_registry = registry.Registry()
 
1838
 
 
1839
author_list_registry.register('all', author_list_all,
 
1840
                              'All authors')
 
1841
 
 
1842
author_list_registry.register('first', author_list_first,
 
1843
                              'The first author')
 
1844
 
 
1845
author_list_registry.register('committer', author_list_committer,
 
1846
                              'The committer')
1684
1847
 
1685
1848
 
1686
1849
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1751
1914
    old_revisions = set()
1752
1915
    new_history = []
1753
1916
    new_revisions = set()
1754
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1755
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1917
    graph = repository.get_graph()
 
1918
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1919
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1756
1920
    stop_revision = None
1757
1921
    do_old = True
1758
1922
    do_new = True
1833
1997
        lf.log_revision(lr)
1834
1998
 
1835
1999
 
1836
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2000
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1837
2001
    """Find file-ids and kinds given a list of files and a revision range.
1838
2002
 
1839
2003
    We search for files at the end of the range. If not found there,
1843
2007
    :param file_list: the list of paths given on the command line;
1844
2008
      the first of these can be a branch location or a file path,
1845
2009
      the remainder must be file paths
 
2010
    :param add_cleanup: When the branch returned is read locked,
 
2011
      an unlock call will be queued to the cleanup.
1846
2012
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1847
2013
      info_list is a list of (relative_path, file_id, kind) tuples where
1848
2014
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
2015
      branch will be read-locked.
1849
2016
    """
1850
 
    from builtins import _get_revision_range, safe_relpath_files
1851
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
2017
    from bzrlib.builtins import _get_revision_range
 
2018
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2019
        file_list[0])
 
2020
    add_cleanup(b.lock_read().unlock)
1852
2021
    # XXX: It's damn messy converting a list of paths to relative paths when
1853
2022
    # those paths might be deleted ones, they might be on a case-insensitive
1854
2023
    # filesystem and/or they might be in silly locations (like another branch).
1858
2027
    # case of running log in a nested directory, assuming paths beyond the
1859
2028
    # first one haven't been deleted ...
1860
2029
    if tree:
1861
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2030
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1862
2031
    else:
1863
2032
        relpaths = [path] + file_list[1:]
1864
2033
    info_list = []
1933
2102
 
1934
2103
properties_handler_registry = registry.Registry()
1935
2104
 
 
2105
# Use the properties handlers to print out bug information if available
 
2106
def _bugs_properties_handler(revision):
 
2107
    if revision.properties.has_key('bugs'):
 
2108
        bug_lines = revision.properties['bugs'].split('\n')
 
2109
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2110
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2111
                          len(row) > 1 and row[1] == 'fixed']
 
2112
 
 
2113
        if fixed_bug_urls:
 
2114
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2115
                    ' '.join(fixed_bug_urls)}
 
2116
    return {}
 
2117
 
 
2118
properties_handler_registry.register('bugs_properties_handler',
 
2119
                                     _bugs_properties_handler)
 
2120
 
1936
2121
 
1937
2122
# adapters which revision ids to log are filtered. When log is called, the
1938
2123
# log_rev_iterator is adapted through each of these factory methods.