~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Patch Queue Manager
  • Date: 2014-02-12 18:22:22 UTC
  • mfrom: (6589.2.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20140212182222-beouo25gaf1cny76
(vila) The XDG Base Directory Specification uses the XDG_CACHE_HOME,
 not XDG_CACHE_DIR. (Andrew Starr-Bochicchio)

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
    foreign,
72
73
    repository as _mod_repository,
73
74
    revision as _mod_revision,
74
75
    revisionspec,
75
 
    trace,
76
76
    tsort,
77
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
78
79
""")
79
80
 
80
81
from bzrlib import (
 
82
    lazy_regex,
81
83
    registry,
82
84
    )
83
85
from bzrlib.osutils import (
84
86
    format_date,
 
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
85
89
    get_terminal_encoding,
86
 
    re_compile_checked,
87
90
    terminal_width,
88
91
    )
89
92
 
102
105
    last_ie = None
103
106
    last_path = None
104
107
    revno = 1
105
 
    for revision_id in branch.revision_history():
106
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
107
 
        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):
108
114
            this_ie = this_inv[file_id]
109
115
            this_path = this_inv.id2path(file_id)
110
116
        else:
132
138
        revno += 1
133
139
 
134
140
 
135
 
def _enumerate_history(branch):
136
 
    rh = []
137
 
    revno = 1
138
 
    for rev_id in branch.revision_history():
139
 
        rh.append((revno, rev_id))
140
 
        revno += 1
141
 
    return rh
142
 
 
143
 
 
144
141
def show_log(branch,
145
142
             lf,
146
143
             specific_fileid=None,
150
147
             end_revision=None,
151
148
             search=None,
152
149
             limit=None,
153
 
             show_diff=False):
 
150
             show_diff=False,
 
151
             match=None):
154
152
    """Write out human-readable log of commits to this branch.
155
153
 
156
154
    This function is being retained for backwards compatibility but
179
177
        if None or 0.
180
178
 
181
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.
182
183
    """
183
184
    # Convert old-style parameters to new-style parameters
184
185
    if specific_fileid is not None:
208
209
    Logger(branch, rqst).show(lf)
209
210
 
210
211
 
211
 
# 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
212
213
# make_log_request_dict() below
213
214
_DEFAULT_REQUEST_PARAMS = {
214
215
    'direction': 'reverse',
215
 
    'levels': 1,
 
216
    'levels': None,
216
217
    'generate_tags': True,
 
218
    'exclude_common_ancestry': False,
217
219
    '_match_using_deltas': True,
218
220
    }
219
221
 
220
222
 
221
223
def make_log_request_dict(direction='reverse', specific_fileids=None,
222
 
    start_revision=None, end_revision=None, limit=None,
223
 
    message_search=None, levels=1, generate_tags=True, delta_type=None,
224
 
    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
                          ):
225
231
    """Convenience function for making a logging request dictionary.
226
232
 
227
233
    Using this function may make code slightly safer by ensuring
247
253
      matching commit messages
248
254
 
249
255
    :param levels: the number of levels of revisions to
250
 
      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.
251
258
 
252
259
    :param generate_tags: If True, include tags for matched revisions.
253
 
 
 
260
`
254
261
    :param delta_type: Either 'full', 'partial' or None.
255
262
      'full' means generate the complete delta - adds/deletes/modifies/etc;
256
263
      'partial' means filter the delta using specific_fileids;
265
272
      algorithm used for matching specific_fileids. This parameter
266
273
      may be removed in the future so bzrlib client code should NOT
267
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
 
268
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] }
269
298
    return {
270
299
        'direction': direction,
271
300
        'specific_fileids': specific_fileids,
272
301
        'start_revision': start_revision,
273
302
        'end_revision': end_revision,
274
303
        'limit': limit,
275
 
        'message_search': message_search,
276
304
        'levels': levels,
277
305
        'generate_tags': generate_tags,
278
306
        'delta_type': delta_type,
279
307
        'diff_type': diff_type,
 
308
        'exclude_common_ancestry': exclude_common_ancestry,
 
309
        'signature': signature,
 
310
        'match': match,
 
311
        'omit_merges': omit_merges,
280
312
        # Add 'private' attributes for features that may be deprecated
281
313
        '_match_using_deltas': _match_using_deltas,
282
314
    }
284
316
 
285
317
def _apply_log_request_defaults(rqst):
286
318
    """Apply default values to a request dictionary."""
287
 
    result = _DEFAULT_REQUEST_PARAMS
 
319
    result = _DEFAULT_REQUEST_PARAMS.copy()
288
320
    if rqst:
289
321
        result.update(rqst)
290
322
    return result
291
323
 
292
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 "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
 
293
346
class LogGenerator(object):
294
347
    """A generator of log revisions."""
295
348
 
302
355
 
303
356
 
304
357
class Logger(object):
305
 
    """An object the generates, formats and displays a log."""
 
358
    """An object that generates, formats and displays a log."""
306
359
 
307
360
    def __init__(self, branch, rqst):
308
361
        """Create a Logger.
340
393
        # Tweak the LogRequest based on what the LogFormatter can handle.
341
394
        # (There's no point generating stuff if the formatter can't display it.)
342
395
        rqst = self.rqst
343
 
        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
 
344
400
        if not getattr(lf, 'supports_tags', False):
345
401
            rqst['generate_tags'] = False
346
402
        if not getattr(lf, 'supports_delta', False):
347
403
            rqst['delta_type'] = None
348
404
        if not getattr(lf, 'supports_diff', False):
349
405
            rqst['diff_type'] = None
 
406
        if not getattr(lf, 'supports_signatures', False):
 
407
            rqst['signature'] = False
350
408
 
351
409
        # Find and print the interesting revisions
352
410
        generator = self._generator_factory(self.branch, rqst)
356
414
 
357
415
    def _generator_factory(self, branch, rqst):
358
416
        """Make the LogGenerator object to use.
359
 
        
 
417
 
360
418
        Subclasses may wish to override this.
361
419
        """
362
420
        return _DefaultLogGenerator(branch, rqst)
383
441
        :return: An iterator yielding LogRevision objects.
384
442
        """
385
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')
386
449
        log_count = 0
387
450
        revision_iterator = self._create_log_revision_iterator()
388
451
        for revs in revision_iterator:
389
452
            for (rev_id, revno, merge_depth), rev, delta in revs:
390
453
                # 0 levels means show everything; merge_depth counts from 0
391
 
                levels = rqst.get('levels')
392
454
                if levels != 0 and merge_depth >= levels:
393
455
                    continue
394
 
                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
395
467
                yield LogRevision(rev, revno, merge_depth, delta,
396
 
                    self.rev_tag_dict.get(rev_id), diff)
397
 
                limit = rqst.get('limit')
 
468
                    self.rev_tag_dict.get(rev_id), diff, signature)
398
469
                if limit:
399
470
                    log_count += 1
400
471
                    if log_count >= limit:
401
472
                        return
402
473
 
403
 
    def _format_diff(self, rev, rev_id):
404
 
        diff_type = self.rqst.get('diff_type')
405
 
        if diff_type is None:
406
 
            return None
 
474
    def _format_diff(self, rev, rev_id, diff_type):
407
475
        repo = self.branch.repository
408
476
        if len(rev.parent_ids) == 0:
409
477
            ancestor_id = _mod_revision.NULL_REVISION
417
485
        else:
418
486
            specific_files = None
419
487
        s = StringIO()
 
488
        path_encoding = get_diff_header_encoding()
420
489
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
421
 
            new_label='')
 
490
            new_label='', path_encoding=path_encoding)
422
491
        return s.getvalue()
423
492
 
424
493
    def _create_log_revision_iterator(self):
448
517
        generate_merge_revisions = rqst.get('levels') != 1
449
518
        delayed_graph_generation = not rqst.get('specific_fileids') and (
450
519
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
451
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
452
 
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
453
 
            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'))
454
526
 
455
527
        # Apply the other filters
456
528
        return make_log_rev_iterator(self.branch, view_revisions,
457
 
            rqst.get('delta_type'), rqst.get('message_search'),
 
529
            rqst.get('delta_type'), rqst.get('match'),
458
530
            file_ids=rqst.get('specific_fileids'),
459
531
            direction=rqst.get('direction'))
460
532
 
463
535
        # Note that we always generate the merge revisions because
464
536
        # filter_revisions_touching_file_id() requires them ...
465
537
        rqst = self.rqst
466
 
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
467
 
            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'))
468
542
        if not isinstance(view_revisions, list):
469
543
            view_revisions = list(view_revisions)
470
544
        view_revisions = _filter_revisions_touching_file_id(self.branch,
471
545
            rqst.get('specific_fileids')[0], view_revisions,
472
546
            include_merges=rqst.get('levels') != 1)
473
547
        return make_log_rev_iterator(self.branch, view_revisions,
474
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
548
            rqst.get('delta_type'), rqst.get('match'))
475
549
 
476
550
 
477
551
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
478
 
    generate_merge_revisions, delayed_graph_generation=False):
 
552
                         generate_merge_revisions,
 
553
                         delayed_graph_generation=False,
 
554
                         exclude_common_ancestry=False,
 
555
                         ):
479
556
    """Calculate the revisions to view.
480
557
 
481
558
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
482
559
             a list of the same tuples.
483
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)
484
566
    br_revno, br_rev_id = branch.last_revision_info()
485
567
    if br_revno == 0:
486
568
        return []
487
569
 
488
 
    # If a single revision is requested, check we can handle it
489
 
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
490
 
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
491
 
    if generate_single_revision:
492
 
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
493
 
 
494
 
    # 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)
495
576
    if not generate_merge_revisions:
496
 
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
497
 
            direction)
498
 
    else:
499
 
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
500
 
            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
501
601
 
502
602
 
503
603
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
505
605
        # It's the tip
506
606
        return [(br_rev_id, br_revno, 0)]
507
607
    else:
508
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
509
 
        revno_str = '.'.join(str(n) for n in revno)
 
608
        revno_str = _compute_revno_str(branch, rev_id)
510
609
        return [(rev_id, revno_str, 0)]
511
610
 
512
611
 
513
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
514
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
515
 
    # If a start limit was given and it's not obviously an
516
 
    # ancestor of the end limit, check it before outputting anything
517
 
    if direction == 'forward' or (start_rev_id
518
 
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
519
 
        try:
520
 
            result = list(result)
521
 
        except _StartNotLinearAncestor:
522
 
            raise errors.BzrCommandError('Start revision not found in'
523
 
                ' left-hand history of end revision.')
524
 
    if direction == 'forward':
525
 
        result = reversed(result)
526
 
    return result
527
 
 
528
 
 
529
612
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
530
 
    delayed_graph_generation):
 
613
                            delayed_graph_generation,
 
614
                            exclude_common_ancestry=False):
531
615
    # On large trees, generating the merge graph can take 30-60 seconds
532
616
    # so we delay doing it until a merge is detected, incrementally
533
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
534
622
    initial_revisions = []
535
623
    if delayed_graph_generation:
536
624
        try:
537
 
            for rev_id, revno, depth in \
538
 
                _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):
539
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.
540
643
                    end_rev_id = rev_id
541
644
                    break
542
645
                else:
543
646
                    initial_revisions.append((rev_id, revno, depth))
544
647
            else:
545
648
                # No merged revisions found
546
 
                if direction == 'reverse':
547
 
                    return initial_revisions
548
 
                elif direction == 'forward':
549
 
                    return reversed(initial_revisions)
550
 
                else:
551
 
                    raise ValueError('invalid direction %r' % direction)
 
649
                return initial_revisions
552
650
        except _StartNotLinearAncestor:
553
651
            # A merge was never detected so the lower revision limit can't
554
652
            # be nested down somewhere
555
 
            raise errors.BzrCommandError('Start revision not found in'
556
 
                ' 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.
557
658
 
558
659
    # A log including nested merges is required. If the direction is reverse,
559
660
    # we rebase the initial merge depths so that the development line is
562
663
    # indented at the end seems slightly nicer in that case.
563
664
    view_revisions = chain(iter(initial_revisions),
564
665
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
565
 
        rebase_initial_depths=direction == 'reverse'))
566
 
    if direction == 'reverse':
567
 
        return view_revisions
568
 
    elif direction == 'forward':
569
 
        # Forward means oldest first, adjusting for depth.
570
 
        view_revisions = reverse_by_depth(list(view_revisions))
571
 
        return _rebase_merge_depth(view_revisions)
572
 
    else:
573
 
        raise ValueError('invalid direction %r' % direction)
 
666
                              rebase_initial_depths=(direction == 'reverse'),
 
667
                              exclude_common_ancestry=exclude_common_ancestry))
 
668
    return view_revisions
574
669
 
575
670
 
576
671
def _has_merges(branch, rev_id):
579
674
    return len(parents) > 1
580
675
 
581
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
 
582
692
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
583
693
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
584
694
    if start_rev_id and end_rev_id:
585
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
586
 
        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
587
701
        if len(start_dotted) == 1 and len(end_dotted) == 1:
588
702
            # both on mainline
589
703
            return start_dotted[0] <= end_dotted[0]
594
708
        else:
595
709
            # not obvious
596
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.
597
713
    return True
598
714
 
599
715
 
600
 
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):
601
718
    """Calculate a sequence of revisions to view, newest to oldest.
602
719
 
603
720
    :param start_rev_id: the lower revision-id
604
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.
605
724
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
606
725
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
607
 
      is not found walking the left-hand history
 
726
        is not found walking the left-hand history
608
727
    """
609
728
    br_revno, br_rev_id = branch.last_revision_info()
610
729
    repo = branch.repository
 
730
    graph = repo.get_graph()
611
731
    if start_rev_id is None and end_rev_id is None:
612
732
        cur_revno = br_revno
613
 
        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,)):
614
735
            yield revision_id, str(cur_revno), 0
615
736
            cur_revno -= 1
616
737
    else:
617
738
        if end_rev_id is None:
618
739
            end_rev_id = br_rev_id
619
740
        found_start = start_rev_id is None
620
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
621
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
622
 
            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)
623
744
            if not found_start and revision_id == start_rev_id:
624
 
                yield revision_id, revno_str, 0
 
745
                if not exclude_common_ancestry:
 
746
                    yield revision_id, revno_str, 0
625
747
                found_start = True
626
748
                break
627
749
            else:
632
754
 
633
755
 
634
756
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
635
 
    rebase_initial_depths=True):
 
757
                          rebase_initial_depths=True,
 
758
                          exclude_common_ancestry=False):
636
759
    """Calculate revisions to view including merges, newest to oldest.
637
760
 
638
761
    :param branch: the branch
642
765
      revision is found?
643
766
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
644
767
    """
 
768
    if exclude_common_ancestry:
 
769
        stop_rule = 'with-merges-without-common-ancestry'
 
770
    else:
 
771
        stop_rule = 'with-merges'
645
772
    view_revisions = branch.iter_merge_sorted_revisions(
646
773
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
647
 
        stop_rule="with-merges")
 
774
        stop_rule=stop_rule)
648
775
    if not rebase_initial_depths:
649
776
        for (rev_id, merge_depth, revno, end_of_merge
650
777
             ) in view_revisions:
661
788
                depth_adjustment = merge_depth
662
789
            if depth_adjustment:
663
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.
664
795
                    depth_adjustment = merge_depth
665
796
                merge_depth -= depth_adjustment
666
797
            yield rev_id, '.'.join(map(str, revno)), merge_depth
667
798
 
668
799
 
669
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
670
 
        specific_fileid, generate_merge_revisions):
671
 
    """Calculate the revisions to view.
672
 
 
673
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
674
 
             a list of the same tuples.
675
 
    """
676
 
    # This method is no longer called by the main code path.
677
 
    # It is retained for API compatibility and may be deprecated
678
 
    # soon. IGC 20090116
679
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
680
 
        end_revision)
681
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
682
 
        direction, generate_merge_revisions or specific_fileid))
683
 
    if specific_fileid:
684
 
        view_revisions = _filter_revisions_touching_file_id(branch,
685
 
            specific_fileid, view_revisions,
686
 
            include_merges=generate_merge_revisions)
687
 
    return _rebase_merge_depth(view_revisions)
688
 
 
689
 
 
690
800
def _rebase_merge_depth(view_revisions):
691
801
    """Adjust depths upwards so the top level is 0."""
692
802
    # If either the first or last revision have a merge_depth of 0, we're done
736
846
    return log_rev_iterator
737
847
 
738
848
 
739
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
849
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
740
850
    """Create a filtered iterator of log_rev_iterator matching on a regex.
741
851
 
742
852
    :param branch: The branch being logged.
743
853
    :param generate_delta: Whether to generate a delta for each revision.
744
 
    :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.
745
858
    :param log_rev_iterator: An input iterator containing all revisions that
746
859
        could be displayed, in lists.
747
860
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
748
861
        delta).
749
862
    """
750
 
    if search is None:
 
863
    if match is None:
751
864
        return log_rev_iterator
752
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
753
 
            'log message filter')
754
 
    return _filter_message_re(searchRE, log_rev_iterator)
755
 
 
756
 
 
757
 
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):
758
871
    for revs in log_rev_iterator:
759
 
        new_revs = []
760
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
761
 
            if searchRE.search(rev.message):
762
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
763
 
        yield new_revs
764
 
 
 
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])
765
892
 
766
893
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
767
894
    fileids=None, direction='reverse'):
840
967
 
841
968
def _update_fileids(delta, fileids, stop_on):
842
969
    """Update the set of file-ids to search based on file lifecycle events.
843
 
    
 
970
 
844
971
    :param fileids: a set of fileids to update
845
972
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
846
973
      fileids set once their add or remove entry is detected respectively
887
1014
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
888
1015
        delta).
889
1016
    """
890
 
    repository = branch.repository
891
1017
    num = 9
892
1018
    for batch in log_rev_iterator:
893
1019
        batch = iter(batch)
942
1068
    if branch_revno != 0:
943
1069
        if (start_rev_id == _mod_revision.NULL_REVISION
944
1070
            or end_rev_id == _mod_revision.NULL_REVISION):
945
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1071
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
946
1072
        if start_revno > end_revno:
947
 
            raise errors.BzrCommandError("Start revision must be older than "
948
 
                                         "the end revision.")
 
1073
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1074
                                         "older than the end revision."))
949
1075
    return (start_rev_id, end_rev_id)
950
1076
 
951
1077
 
1000
1126
 
1001
1127
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1002
1128
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1003
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1129
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1004
1130
    if start_revno > end_revno:
1005
 
        raise errors.BzrCommandError("Start revision must be older than "
1006
 
                                     "the end revision.")
 
1131
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1132
                                     "than the end revision."))
1007
1133
 
1008
1134
    if end_revno < start_revno:
1009
1135
        return None, None, None, None
1010
1136
    cur_revno = branch_revno
1011
1137
    rev_nos = {}
1012
1138
    mainline_revs = []
1013
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1014
 
                        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,)):
1015
1142
        if cur_revno < start_revno:
1016
1143
            # We have gone far enough, but we always add 1 more revision
1017
1144
            rev_nos[revision_id] = cur_revno
1031
1158
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1032
1159
 
1033
1160
 
1034
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1035
 
    """Filter view_revisions based on revision ranges.
1036
 
 
1037
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1038
 
            tuples to be filtered.
1039
 
 
1040
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1041
 
            If NONE then all revisions up to the end_rev_id are logged.
1042
 
 
1043
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1044
 
            If NONE then all revisions up to the end of the log are logged.
1045
 
 
1046
 
    :return: The filtered view_revisions.
1047
 
    """
1048
 
    # This method is no longer called by the main code path.
1049
 
    # It may be removed soon. IGC 20090127
1050
 
    if start_rev_id or end_rev_id:
1051
 
        revision_ids = [r for r, n, d in view_revisions]
1052
 
        if start_rev_id:
1053
 
            start_index = revision_ids.index(start_rev_id)
1054
 
        else:
1055
 
            start_index = 0
1056
 
        if start_rev_id == end_rev_id:
1057
 
            end_index = start_index
1058
 
        else:
1059
 
            if end_rev_id:
1060
 
                end_index = revision_ids.index(end_rev_id)
1061
 
            else:
1062
 
                end_index = len(view_revisions) - 1
1063
 
        # To include the revisions merged into the last revision,
1064
 
        # extend end_rev_id down to, but not including, the next rev
1065
 
        # with the same or lesser merge_depth
1066
 
        end_merge_depth = view_revisions[end_index][2]
1067
 
        try:
1068
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1069
 
                if view_revisions[index][2] <= end_merge_depth:
1070
 
                    end_index = index - 1
1071
 
                    break
1072
 
        except IndexError:
1073
 
            # if the search falls off the end then log to the end as well
1074
 
            end_index = len(view_revisions) - 1
1075
 
        view_revisions = view_revisions[start_index:end_index+1]
1076
 
    return view_revisions
1077
 
 
1078
 
 
1079
1161
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1080
1162
    include_merges=True):
1081
1163
    r"""Return the list of revision ids which touch a given file id.
1084
1166
    This includes the revisions which directly change the file id,
1085
1167
    and the revisions which merge these changes. So if the
1086
1168
    revision graph is::
 
1169
 
1087
1170
        A-.
1088
1171
        |\ \
1089
1172
        B C E
1116
1199
    """
1117
1200
    # Lookup all possible text keys to determine which ones actually modified
1118
1201
    # the file.
 
1202
    graph = branch.repository.get_file_graph()
 
1203
    get_parent_map = graph.get_parent_map
1119
1204
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1120
1205
    next_keys = None
1121
1206
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1125
1210
    #       indexing layer. We might consider passing in hints as to the known
1126
1211
    #       access pattern (sparse/clustered, high success rate/low success
1127
1212
    #       rate). This particular access is clustered with a low success rate.
1128
 
    get_parent_map = branch.repository.texts.get_parent_map
1129
1213
    modified_text_revisions = set()
1130
1214
    chunk_size = 1000
1131
1215
    for start in xrange(0, len(text_keys), chunk_size):
1158
1242
    return result
1159
1243
 
1160
1244
 
1161
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1162
 
                       include_merges=True):
1163
 
    """Produce an iterator of revisions to show
1164
 
    :return: an iterator of (revision_id, revno, merge_depth)
1165
 
    (if there is no revno for a revision, None is supplied)
1166
 
    """
1167
 
    # This method is no longer called by the main code path.
1168
 
    # It is retained for API compatibility and may be deprecated
1169
 
    # soon. IGC 20090127
1170
 
    if not include_merges:
1171
 
        revision_ids = mainline_revs[1:]
1172
 
        if direction == 'reverse':
1173
 
            revision_ids.reverse()
1174
 
        for revision_id in revision_ids:
1175
 
            yield revision_id, str(rev_nos[revision_id]), 0
1176
 
        return
1177
 
    graph = branch.repository.get_graph()
1178
 
    # This asks for all mainline revisions, which means we only have to spider
1179
 
    # sideways, rather than depth history. That said, its still size-of-history
1180
 
    # and should be addressed.
1181
 
    # mainline_revisions always includes an extra revision at the beginning, so
1182
 
    # don't request it.
1183
 
    parent_map = dict(((key, value) for key, value in
1184
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1185
 
    # filter out ghosts; merge_sort errors on ghosts.
1186
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1187
 
    merge_sorted_revisions = tsort.merge_sort(
1188
 
        rev_graph,
1189
 
        mainline_revs[-1],
1190
 
        mainline_revs,
1191
 
        generate_revno=True)
1192
 
 
1193
 
    if direction == 'forward':
1194
 
        # forward means oldest first.
1195
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1196
 
    elif direction != 'reverse':
1197
 
        raise ValueError('invalid direction %r' % direction)
1198
 
 
1199
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1200
 
         ) in merge_sorted_revisions:
1201
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1202
 
 
1203
 
 
1204
1245
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1205
1246
    """Reverse revisions by depth.
1206
1247
 
1241
1282
    """
1242
1283
 
1243
1284
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1244
 
                 tags=None, diff=None):
 
1285
                 tags=None, diff=None, signature=None):
1245
1286
        self.rev = rev
1246
 
        self.revno = str(revno)
 
1287
        if revno is None:
 
1288
            self.revno = None
 
1289
        else:
 
1290
            self.revno = str(revno)
1247
1291
        self.merge_depth = merge_depth
1248
1292
        self.delta = delta
1249
1293
        self.tags = tags
1250
1294
        self.diff = diff
 
1295
        self.signature = signature
1251
1296
 
1252
1297
 
1253
1298
class LogFormatter(object):
1262
1307
    to indicate which LogRevision attributes it supports:
1263
1308
 
1264
1309
    - supports_delta must be True if this log formatter supports delta.
1265
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1266
 
        attribute describes whether the 'short_status' format (1) or the long
1267
 
        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.
1268
1313
 
1269
1314
    - supports_merge_revisions must be True if this log formatter supports
1270
 
        merge revisions.  If not, then only mainline revisions will be passed
1271
 
        to the formatter.
 
1315
      merge revisions.  If not, then only mainline revisions will be passed
 
1316
      to the formatter.
1272
1317
 
1273
1318
    - preferred_levels is the number of levels this formatter defaults to.
1274
 
        The default value is zero meaning display all levels.
1275
 
        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.
1276
1321
 
1277
1322
    - supports_tags must be True if this log formatter supports tags.
1278
 
        Otherwise the tags attribute may not be populated.
 
1323
      Otherwise the tags attribute may not be populated.
1279
1324
 
1280
1325
    - supports_diff must be True if this log formatter supports diffs.
1281
 
        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.
1282
1330
 
1283
1331
    Plugins can register functions to show custom revision properties using
1284
1332
    the properties_handler_registry. The registered function
1285
 
    must respect the following interface description:
 
1333
    must respect the following interface description::
 
1334
 
1286
1335
        def my_show_properties(properties_dict):
1287
1336
            # code that returns a dict {'name':'value'} of the properties
1288
1337
            # to be shown
1290
1339
    preferred_levels = 0
1291
1340
 
1292
1341
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1293
 
                 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):
1294
1344
        """Create a LogFormatter.
1295
1345
 
1296
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.
1297
1349
        :param show_ids: if True, revision-ids are to be displayed
1298
1350
        :param show_timezone: the timezone to use
1299
1351
        :param delta_format: the level of delta information to display
1302
1354
          let the log formatter decide.
1303
1355
        :param show_advice: whether to show advice at the end of the
1304
1356
          log or not
 
1357
        :param author_list_handler: callable generating a list of
 
1358
          authors to display for a given revision
1305
1359
        """
1306
1360
        self.to_file = to_file
1307
1361
        # 'exact' stream used to show diff, it should print content 'as is'
1308
1362
        # and should not try to decode/encode it to unicode to avoid bug #328007
1309
 
        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)
1310
1370
        self.show_ids = show_ids
1311
1371
        self.show_timezone = show_timezone
1312
1372
        if delta_format is None:
1316
1376
        self.levels = levels
1317
1377
        self._show_advice = show_advice
1318
1378
        self._merge_count = 0
 
1379
        self._author_list_handler = author_list_handler
1319
1380
 
1320
1381
    def get_levels(self):
1321
1382
        """Get the number of levels to display or 0 for all."""
1340
1401
            if advice_sep:
1341
1402
                self.to_file.write(advice_sep)
1342
1403
            self.to_file.write(
1343
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1404
                "Use --include-merged or -n0 to see merged revisions.\n")
1344
1405
 
1345
1406
    def get_advice_separator(self):
1346
1407
        """Get the text separating the log from the closing advice."""
1353
1414
        return address
1354
1415
 
1355
1416
    def short_author(self, rev):
1356
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1357
 
        if name:
1358
 
            return name
1359
 
        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
1360
1452
 
1361
1453
    def merge_marker(self, revision):
1362
1454
        """Get the merge marker to include in the output or '' if none."""
1371
1463
 
1372
1464
        If a registered handler raises an error it is propagated.
1373
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)
1374
1477
        for key, handler in properties_handler_registry.iteritems():
1375
 
            for key, value in handler(revision).items():
1376
 
                self.to_file.write(indent + key + ': ' + value + '\n')
 
1478
            lines.extend(self._format_properties(handler(revision)))
 
1479
        return lines
 
1480
 
 
1481
    def _foreign_info_properties(self, rev):
 
1482
        """Custom log displayer for foreign revision identifiers.
 
1483
 
 
1484
        :param rev: Revision object.
 
1485
        """
 
1486
        # Revision comes directly from a foreign repository
 
1487
        if isinstance(rev, foreign.ForeignRevision):
 
1488
            return self._format_properties(
 
1489
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1490
 
 
1491
        # Imported foreign revision revision ids always contain :
 
1492
        if not ":" in rev.revision_id:
 
1493
            return []
 
1494
 
 
1495
        # Revision was once imported from a foreign repository
 
1496
        try:
 
1497
            foreign_revid, mapping = \
 
1498
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
 
1499
        except errors.InvalidRevisionId:
 
1500
            return []
 
1501
 
 
1502
        return self._format_properties(
 
1503
            mapping.vcs.show_foreign_revid(foreign_revid))
 
1504
 
 
1505
    def _format_properties(self, properties):
 
1506
        lines = []
 
1507
        for key, value in properties.items():
 
1508
            lines.append(key + ': ' + value)
 
1509
        return lines
1377
1510
 
1378
1511
    def show_diff(self, to_file, diff, indent):
1379
1512
        for l in diff.rstrip().split('\n'):
1380
1513
            to_file.write(indent + '%s\n' % (l,))
1381
1514
 
1382
1515
 
 
1516
# Separator between revisions in long format
 
1517
_LONG_SEP = '-' * 60
 
1518
 
 
1519
 
1383
1520
class LongLogFormatter(LogFormatter):
1384
1521
 
1385
1522
    supports_merge_revisions = True
1387
1524
    supports_delta = True
1388
1525
    supports_tags = True
1389
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)
1390
1543
 
1391
1544
    def log_revision(self, revision):
1392
1545
        """Log a revision, either merged or not."""
1393
1546
        indent = '    ' * revision.merge_depth
1394
 
        to_file = self.to_file
1395
 
        to_file.write(indent + '-' * 60 + '\n')
 
1547
        lines = [_LONG_SEP]
1396
1548
        if revision.revno is not None:
1397
 
            to_file.write(indent + 'revno: %s%s\n' % (revision.revno,
 
1549
            lines.append('revno: %s%s' % (revision.revno,
1398
1550
                self.merge_marker(revision)))
1399
1551
        if revision.tags:
1400
 
            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,))
1401
1555
        if self.show_ids:
1402
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
1403
 
            to_file.write('\n')
1404
1556
            for parent_id in revision.rev.parent_ids:
1405
 
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
1406
 
        self.show_properties(revision.rev, indent)
 
1557
                lines.append('parent: %s' % (parent_id,))
 
1558
        lines.extend(self.custom_properties(revision.rev))
1407
1559
 
1408
1560
        committer = revision.rev.committer
1409
 
        authors = revision.rev.get_apparent_authors()
 
1561
        authors = self.authors(revision.rev, 'all')
1410
1562
        if authors != [committer]:
1411
 
            to_file.write(indent + 'author: %s\n' % (", ".join(authors),))
1412
 
        to_file.write(indent + 'committer: %s\n' % (committer,))
 
1563
            lines.append('author: %s' % (", ".join(authors),))
 
1564
        lines.append('committer: %s' % (committer,))
1413
1565
 
1414
1566
        branch_nick = revision.rev.properties.get('branch-nick', None)
1415
1567
        if branch_nick is not None:
1416
 
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
1417
 
 
1418
 
        date_str = format_date(revision.rev.timestamp,
1419
 
                               revision.rev.timezone or 0,
1420
 
                               self.show_timezone)
1421
 
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
1422
 
 
1423
 
        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:')
1424
1576
        if not revision.rev.message:
1425
 
            to_file.write(indent + '  (no message)\n')
 
1577
            lines.append('  (no message)')
1426
1578
        else:
1427
1579
            message = revision.rev.message.rstrip('\r\n')
1428
1580
            for l in message.split('\n'):
1429
 
                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)))
1430
1586
        if revision.delta is not None:
1431
 
            # We don't respect delta_format for compatibility
1432
 
            revision.delta.show(to_file, self.show_ids, indent=indent,
1433
 
                                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)
1434
1591
        if revision.diff is not None:
1435
1592
            to_file.write(indent + 'diff:\n')
 
1593
            to_file.flush()
1436
1594
            # Note: we explicitly don't indent the diff (relative to the
1437
1595
            # revision information) so that the output can be fed to patch -p0
1438
1596
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1597
            self.to_exact_file.flush()
1439
1598
 
1440
1599
    def get_advice_separator(self):
1441
1600
        """Get the text separating the log from the closing advice."""
1465
1624
        indent = '    ' * depth
1466
1625
        revno_width = self.revno_width_by_depth.get(depth)
1467
1626
        if revno_width is None:
1468
 
            if revision.revno.find('.') == -1:
 
1627
            if revision.revno is None or revision.revno.find('.') == -1:
1469
1628
                # mainline revno, e.g. 12345
1470
1629
                revno_width = 5
1471
1630
            else:
1479
1638
        if revision.tags:
1480
1639
            tags = ' {%s}' % (', '.join(revision.tags))
1481
1640
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1482
 
                revision.revno, self.short_author(revision.rev),
 
1641
                revision.revno or "", self.short_author(revision.rev),
1483
1642
                format_date(revision.rev.timestamp,
1484
1643
                            revision.rev.timezone or 0,
1485
1644
                            self.show_timezone, date_fmt="%Y-%m-%d",
1486
1645
                            show_offset=False),
1487
1646
                tags, self.merge_marker(revision)))
1488
1647
        self.show_properties(revision.rev, indent+offset)
1489
 
        if self.show_ids:
 
1648
        if self.show_ids or revision.revno is None:
1490
1649
            to_file.write(indent + offset + 'revision-id:%s\n'
1491
1650
                          % (revision.rev.revision_id,))
1492
1651
        if not revision.rev.message:
1497
1656
                to_file.write(indent + offset + '%s\n' % (l,))
1498
1657
 
1499
1658
        if revision.delta is not None:
1500
 
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
1501
 
                                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)
1502
1664
        if revision.diff is not None:
1503
1665
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1504
1666
        to_file.write('\n')
1512
1674
 
1513
1675
    def __init__(self, *args, **kwargs):
1514
1676
        super(LineLogFormatter, self).__init__(*args, **kwargs)
1515
 
        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
1516
1682
 
1517
1683
    def truncate(self, str, max_len):
1518
 
        if len(str) <= max_len:
 
1684
        if max_len is None or len(str) <= max_len:
1519
1685
            return str
1520
 
        return str[:max_len-3]+'...'
 
1686
        return str[:max_len-3] + '...'
1521
1687
 
1522
1688
    def date_string(self, rev):
1523
1689
        return format_date(rev.timestamp, rev.timezone or 0,
1538
1704
 
1539
1705
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1540
1706
        """Format log info into one string. Truncate tail of string
1541
 
        :param  revno:      revision number or None.
1542
 
                            Revision numbers counts from 1.
1543
 
        :param  rev:        revision object
1544
 
        :param  max_chars:  maximum length of resulting string
1545
 
        :param  tags:       list of tags or None
1546
 
        :param  prefix:     string to prefix each line
1547
 
        :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
1548
1715
        """
1549
1716
        out = []
1550
1717
        if revno:
1551
1718
            # show revno only when is not None
1552
1719
            out.append("%s:" % revno)
1553
 
        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))
1554
1724
        out.append(self.date_string(rev))
1555
1725
        if len(rev.parent_ids) > 1:
1556
1726
            out.append('[merge]')
1575
1745
                               self.show_timezone,
1576
1746
                               date_fmt='%Y-%m-%d',
1577
1747
                               show_offset=False)
1578
 
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1748
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1749
        committer_str = committer_str.replace(' <', '  <')
1579
1750
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1580
1751
 
1581
1752
        if revision.delta is not None and revision.delta.has_changed():
1614
1785
        return self.get(name)(*args, **kwargs)
1615
1786
 
1616
1787
    def get_default(self, branch):
1617
 
        return self.get(branch.get_config().log_format())
 
1788
        c = branch.get_config_stack()
 
1789
        return self.get(c.get('log_format'))
1618
1790
 
1619
1791
 
1620
1792
log_formatter_registry = LogFormatterRegistry()
1621
1793
 
1622
1794
 
1623
1795
log_formatter_registry.register('short', ShortLogFormatter,
1624
 
                                'Moderately short log format')
 
1796
                                'Moderately short log format.')
1625
1797
log_formatter_registry.register('long', LongLogFormatter,
1626
 
                                'Detailed log format')
 
1798
                                'Detailed log format.')
1627
1799
log_formatter_registry.register('line', LineLogFormatter,
1628
 
                                'Log format with one line per revision')
 
1800
                                'Log format with one line per revision.')
1629
1801
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1630
 
                                'Format used by GNU ChangeLog files')
 
1802
                                'Format used by GNU ChangeLog files.')
1631
1803
 
1632
1804
 
1633
1805
def register_formatter(name, formatter):
1643
1815
    try:
1644
1816
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1645
1817
    except KeyError:
1646
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1647
 
 
1648
 
 
1649
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1650
 
    # deprecated; for compatibility
1651
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1652
 
    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')
1653
1847
 
1654
1848
 
1655
1849
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1720
1914
    old_revisions = set()
1721
1915
    new_history = []
1722
1916
    new_revisions = set()
1723
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1724
 
    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)
1725
1920
    stop_revision = None
1726
1921
    do_old = True
1727
1922
    do_new = True
1802
1997
        lf.log_revision(lr)
1803
1998
 
1804
1999
 
1805
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2000
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1806
2001
    """Find file-ids and kinds given a list of files and a revision range.
1807
2002
 
1808
2003
    We search for files at the end of the range. If not found there,
1812
2007
    :param file_list: the list of paths given on the command line;
1813
2008
      the first of these can be a branch location or a file path,
1814
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.
1815
2012
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1816
2013
      info_list is a list of (relative_path, file_id, kind) tuples where
1817
2014
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
2015
      branch will be read-locked.
1818
2016
    """
1819
 
    from builtins import _get_revision_range, safe_relpath_files
1820
 
    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)
1821
2021
    # XXX: It's damn messy converting a list of paths to relative paths when
1822
2022
    # those paths might be deleted ones, they might be on a case-insensitive
1823
2023
    # filesystem and/or they might be in silly locations (like another branch).
1827
2027
    # case of running log in a nested directory, assuming paths beyond the
1828
2028
    # first one haven't been deleted ...
1829
2029
    if tree:
1830
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2030
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1831
2031
    else:
1832
2032
        relpaths = [path] + file_list[1:]
1833
2033
    info_list = []
1901
2101
 
1902
2102
 
1903
2103
properties_handler_registry = registry.Registry()
1904
 
properties_handler_registry.register_lazy("foreign",
1905
 
                                          "bzrlib.foreign",
1906
 
                                          "show_foreign_properties")
 
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)
1907
2120
 
1908
2121
 
1909
2122
# adapters which revision ids to log are filtered. When log is called, the