/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/log.py

  • Committer: Jelmer Vernooij
  • Date: 2018-11-11 04:08:32 UTC
  • mto: (7143.16.20 even-more-cleanups)
  • mto: This revision was merged to the branch mainline in revision 7175.
  • Revision ID: jelmer@jelmer.uk-20181111040832-nsljjynzzwmznf3h
Run autopep8.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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
 
from cStringIO import StringIO
54
 
from itertools import (
55
 
    chain,
56
 
    izip,
57
 
    )
 
53
import itertools
58
54
import re
59
55
import sys
60
56
from warnings import (
61
57
    warn,
62
58
    )
63
59
 
64
 
from bzrlib.lazy_import import lazy_import
 
60
from .lazy_import import lazy_import
65
61
lazy_import(globals(), """
66
62
 
67
 
from bzrlib import (
68
 
    bzrdir,
 
63
from breezy import (
69
64
    config,
 
65
    controldir,
70
66
    diff,
71
 
    errors,
72
67
    foreign,
73
 
    repository as _mod_repository,
74
68
    revision as _mod_revision,
75
 
    revisionspec,
76
 
    trace,
77
69
    tsort,
78
70
    )
 
71
from breezy.i18n import gettext, ngettext
79
72
""")
80
73
 
81
 
from bzrlib import (
 
74
from . import (
 
75
    errors,
82
76
    registry,
 
77
    revisionspec,
83
78
    )
84
 
from bzrlib.osutils import (
 
79
from .osutils import (
85
80
    format_date,
86
81
    format_date_with_offset_in_original_timezone,
 
82
    get_diff_header_encoding,
87
83
    get_terminal_encoding,
88
 
    re_compile_checked,
89
84
    terminal_width,
90
85
    )
91
 
from bzrlib.symbol_versioning import (
92
 
    deprecated_function,
93
 
    deprecated_in,
 
86
from .sixish import (
 
87
    BytesIO,
 
88
    range,
 
89
    zip,
94
90
    )
95
 
 
96
 
 
97
 
def find_touching_revisions(branch, file_id):
 
91
from .tree import find_previous_path
 
92
 
 
93
 
 
94
def find_touching_revisions(repository, last_revision, last_tree, last_path):
98
95
    """Yield a description of revisions which affect the file_id.
99
96
 
100
97
    Each returned element is (revno, revision_id, description)
105
102
    TODO: Perhaps some way to limit this to only particular revisions,
106
103
    or to traverse a non-mainline set of revisions?
107
104
    """
108
 
    last_ie = None
109
 
    last_path = None
110
 
    revno = 1
111
 
    for revision_id in branch.revision_history():
112
 
        this_inv = branch.repository.get_inventory(revision_id)
113
 
        if file_id in this_inv:
114
 
            this_ie = this_inv[file_id]
115
 
            this_path = this_inv.id2path(file_id)
116
 
        else:
117
 
            this_ie = this_path = None
 
105
    last_verifier = last_tree.get_file_verifier(last_path)
 
106
    graph = repository.get_graph()
 
107
    history = list(graph.iter_lefthand_ancestry(last_revision, []))
 
108
    revno = len(history)
 
109
    for revision_id in history:
 
110
        this_tree = repository.revision_tree(revision_id)
 
111
        this_path = find_previous_path(last_tree, this_tree, last_path)
118
112
 
119
113
        # now we know how it was last time, and how it is in this revision.
120
114
        # are those two states effectively the same or not?
121
 
 
122
 
        if not this_ie and not last_ie:
123
 
            # not present in either
124
 
            pass
125
 
        elif this_ie and not last_ie:
126
 
            yield revno, revision_id, "added " + this_path
127
 
        elif not this_ie and last_ie:
128
 
            # deleted here
129
 
            yield revno, revision_id, "deleted " + last_path
 
115
        if this_path is not None and last_path is None:
 
116
            yield revno, revision_id, "deleted " + this_path
 
117
            this_verifier = this_tree.get_file_verifier(this_path)
 
118
        elif this_path is None and last_path is not None:
 
119
            yield revno, revision_id, "added " + last_path
130
120
        elif this_path != last_path:
131
 
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
132
 
        elif (this_ie.text_size != last_ie.text_size
133
 
              or this_ie.text_sha1 != last_ie.text_sha1):
134
 
            yield revno, revision_id, "modified " + this_path
 
121
            yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
 
122
            this_verifier = this_tree.get_file_verifier(this_path)
 
123
        else:
 
124
            this_verifier = this_tree.get_file_verifier(this_path)
 
125
            if (this_verifier != last_verifier):
 
126
                yield revno, revision_id, "modified " + this_path
135
127
 
136
 
        last_ie = this_ie
 
128
        last_verifier = this_verifier
137
129
        last_path = this_path
138
 
        revno += 1
139
 
 
140
 
 
141
 
def _enumerate_history(branch):
142
 
    rh = []
143
 
    revno = 1
144
 
    for rev_id in branch.revision_history():
145
 
        rh.append((revno, rev_id))
146
 
        revno += 1
147
 
    return rh
 
130
        last_tree = this_tree
 
131
        if last_path is None:
 
132
            return
 
133
        revno -= 1
148
134
 
149
135
 
150
136
def show_log(branch,
156
142
             end_revision=None,
157
143
             search=None,
158
144
             limit=None,
159
 
             show_diff=False):
 
145
             show_diff=False,
 
146
             match=None):
160
147
    """Write out human-readable log of commits to this branch.
161
148
 
162
149
    This function is being retained for backwards compatibility but
185
172
        if None or 0.
186
173
 
187
174
    :param show_diff: If True, output a diff after each revision.
 
175
 
 
176
    :param match: Dictionary of search lists to use when matching revision
 
177
      properties.
188
178
    """
189
179
    # Convert old-style parameters to new-style parameters
190
180
    if specific_fileid is not None:
206
196
    else:
207
197
        diff_type = None
208
198
 
 
199
    if isinstance(start_revision, int):
 
200
        try:
 
201
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
 
202
        except errors.NoSuchRevision:
 
203
            raise errors.InvalidRevisionNumber(start_revision)
 
204
 
 
205
    if isinstance(end_revision, int):
 
206
        try:
 
207
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
 
208
        except errors.NoSuchRevision:
 
209
            raise errors.InvalidRevisionNumber(end_revision)
 
210
 
 
211
    if end_revision is not None and end_revision.revno == 0:
 
212
        raise errors.InvalidRevisionNumber(end_revision.revno)
 
213
 
209
214
    # Build the request and execute it
210
215
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
211
 
        start_revision=start_revision, end_revision=end_revision,
212
 
        limit=limit, message_search=search,
213
 
        delta_type=delta_type, diff_type=diff_type)
 
216
                                 start_revision=start_revision, end_revision=end_revision,
 
217
                                 limit=limit, message_search=search,
 
218
                                 delta_type=delta_type, diff_type=diff_type)
214
219
    Logger(branch, rqst).show(lf)
215
220
 
216
221
 
217
 
# Note: This needs to be kept this in sync with the defaults in
 
222
# Note: This needs to be kept in sync with the defaults in
218
223
# make_log_request_dict() below
219
224
_DEFAULT_REQUEST_PARAMS = {
220
225
    'direction': 'reverse',
221
 
    'levels': 1,
 
226
    'levels': None,
222
227
    'generate_tags': True,
223
228
    'exclude_common_ancestry': False,
224
229
    '_match_using_deltas': True,
227
232
 
228
233
def make_log_request_dict(direction='reverse', specific_fileids=None,
229
234
                          start_revision=None, end_revision=None, limit=None,
230
 
                          message_search=None, levels=1, generate_tags=True,
 
235
                          message_search=None, levels=None, generate_tags=True,
231
236
                          delta_type=None,
232
237
                          diff_type=None, _match_using_deltas=True,
233
 
                          exclude_common_ancestry=False,
 
238
                          exclude_common_ancestry=False, match=None,
 
239
                          signature=False, omit_merges=False,
234
240
                          ):
235
241
    """Convenience function for making a logging request dictionary.
236
242
 
257
263
      matching commit messages
258
264
 
259
265
    :param levels: the number of levels of revisions to
260
 
      generate; 1 for just the mainline; 0 for all levels.
 
266
      generate; 1 for just the mainline; 0 for all levels, or None for
 
267
      a sensible default.
261
268
 
262
269
    :param generate_tags: If True, include tags for matched revisions.
263
 
 
 
270
`
264
271
    :param delta_type: Either 'full', 'partial' or None.
265
272
      'full' means generate the complete delta - adds/deletes/modifies/etc;
266
273
      'partial' means filter the delta using specific_fileids;
273
280
 
274
281
    :param _match_using_deltas: a private parameter controlling the
275
282
      algorithm used for matching specific_fileids. This parameter
276
 
      may be removed in the future so bzrlib client code should NOT
 
283
      may be removed in the future so breezy client code should NOT
277
284
      use it.
278
285
 
279
286
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
280
287
      range operator or as a graph difference.
 
288
 
 
289
    :param signature: show digital signature information
 
290
 
 
291
    :param match: Dictionary of list of search strings to use when filtering
 
292
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
293
      the empty string to match any of the preceding properties.
 
294
 
 
295
    :param omit_merges: If True, commits with more than one parent are
 
296
      omitted.
 
297
 
281
298
    """
 
299
    # Take care of old style message_search parameter
 
300
    if message_search:
 
301
        if match:
 
302
            if 'message' in match:
 
303
                match['message'].append(message_search)
 
304
            else:
 
305
                match['message'] = [message_search]
 
306
        else:
 
307
            match = {'message': [message_search]}
282
308
    return {
283
309
        'direction': direction,
284
310
        'specific_fileids': specific_fileids,
285
311
        'start_revision': start_revision,
286
312
        'end_revision': end_revision,
287
313
        'limit': limit,
288
 
        'message_search': message_search,
289
314
        'levels': levels,
290
315
        'generate_tags': generate_tags,
291
316
        'delta_type': delta_type,
292
317
        'diff_type': diff_type,
293
318
        'exclude_common_ancestry': exclude_common_ancestry,
 
319
        'signature': signature,
 
320
        'match': match,
 
321
        'omit_merges': omit_merges,
294
322
        # Add 'private' attributes for features that may be deprecated
295
323
        '_match_using_deltas': _match_using_deltas,
296
324
    }
298
326
 
299
327
def _apply_log_request_defaults(rqst):
300
328
    """Apply default values to a request dictionary."""
301
 
    result = _DEFAULT_REQUEST_PARAMS
 
329
    result = _DEFAULT_REQUEST_PARAMS.copy()
302
330
    if rqst:
303
331
        result.update(rqst)
304
332
    return result
305
333
 
306
334
 
 
335
def format_signature_validity(rev_id, branch):
 
336
    """get the signature validity
 
337
 
 
338
    :param rev_id: revision id to validate
 
339
    :param branch: branch of revision
 
340
    :return: human readable string to print to log
 
341
    """
 
342
    from breezy import gpg
 
343
 
 
344
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
 
345
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
 
346
    if result[0] == gpg.SIGNATURE_VALID:
 
347
        return u"valid signature from {0}".format(result[1])
 
348
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
349
        return "unknown key {0}".format(result[1])
 
350
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
351
        return "invalid signature!"
 
352
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
353
        return "no signature"
 
354
 
 
355
 
307
356
class LogGenerator(object):
308
357
    """A generator of log revisions."""
309
358
 
336
385
        if not isinstance(lf, LogFormatter):
337
386
            warn("not a LogFormatter instance: %r" % lf)
338
387
 
339
 
        self.branch.lock_read()
340
 
        try:
 
388
        with self.branch.lock_read():
341
389
            if getattr(lf, 'begin_log', None):
342
390
                lf.begin_log()
343
391
            self._show_body(lf)
344
392
            if getattr(lf, 'end_log', None):
345
393
                lf.end_log()
346
 
        finally:
347
 
            self.branch.unlock()
348
394
 
349
395
    def _show_body(self, lf):
350
396
        """Show the main log output.
354
400
        # Tweak the LogRequest based on what the LogFormatter can handle.
355
401
        # (There's no point generating stuff if the formatter can't display it.)
356
402
        rqst = self.rqst
357
 
        rqst['levels'] = lf.get_levels()
 
403
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
404
            # user didn't specify levels, use whatever the LF can handle:
 
405
            rqst['levels'] = lf.get_levels()
 
406
 
358
407
        if not getattr(lf, 'supports_tags', False):
359
408
            rqst['generate_tags'] = False
360
409
        if not getattr(lf, 'supports_delta', False):
361
410
            rqst['delta_type'] = None
362
411
        if not getattr(lf, 'supports_diff', False):
363
412
            rqst['diff_type'] = None
 
413
        if not getattr(lf, 'supports_signatures', False):
 
414
            rqst['signature'] = False
364
415
 
365
416
        # Find and print the interesting revisions
366
417
        generator = self._generator_factory(self.branch, rqst)
367
 
        for lr in generator.iter_log_revisions():
368
 
            lf.log_revision(lr)
 
418
        try:
 
419
            for lr in generator.iter_log_revisions():
 
420
                lf.log_revision(lr)
 
421
        except errors.GhostRevisionUnusableHere:
 
422
            raise errors.BzrCommandError(
 
423
                gettext('Further revision history missing.'))
369
424
        lf.show_advice()
370
425
 
371
426
    def _generator_factory(self, branch, rqst):
372
427
        """Make the LogGenerator object to use.
373
 
        
 
428
 
374
429
        Subclasses may wish to override this.
375
430
        """
376
431
        return _DefaultLogGenerator(branch, rqst)
400
455
        levels = rqst.get('levels')
401
456
        limit = rqst.get('limit')
402
457
        diff_type = rqst.get('diff_type')
 
458
        show_signature = rqst.get('signature')
 
459
        omit_merges = rqst.get('omit_merges')
403
460
        log_count = 0
404
461
        revision_iterator = self._create_log_revision_iterator()
405
462
        for revs in revision_iterator:
406
463
            for (rev_id, revno, merge_depth), rev, delta in revs:
407
464
                # 0 levels means show everything; merge_depth counts from 0
408
 
                if levels != 0 and merge_depth >= levels:
409
 
                    continue
 
465
                if levels != 0 and merge_depth is not None and merge_depth >= levels:
 
466
                    continue
 
467
                if omit_merges and len(rev.parent_ids) > 1:
 
468
                    continue
 
469
                if rev is None:
 
470
                    raise errors.GhostRevisionUnusableHere(rev_id)
410
471
                if diff_type is None:
411
472
                    diff = None
412
473
                else:
413
474
                    diff = self._format_diff(rev, rev_id, diff_type)
 
475
                if show_signature:
 
476
                    signature = format_signature_validity(rev_id, self.branch)
 
477
                else:
 
478
                    signature = None
414
479
                yield LogRevision(rev, revno, merge_depth, delta,
415
 
                    self.rev_tag_dict.get(rev_id), diff)
 
480
                                  self.rev_tag_dict.get(rev_id), diff, signature)
416
481
                if limit:
417
482
                    log_count += 1
418
483
                    if log_count >= limit:
431
496
            specific_files = [tree_2.id2path(id) for id in file_ids]
432
497
        else:
433
498
            specific_files = None
434
 
        s = StringIO()
 
499
        s = BytesIO()
 
500
        path_encoding = get_diff_header_encoding()
435
501
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
436
 
            new_label='')
 
502
                             new_label='', path_encoding=path_encoding)
437
503
        return s.getvalue()
438
504
 
439
505
    def _create_log_revision_iterator(self):
454
520
            file_count = len(self.rqst.get('specific_fileids'))
455
521
            if file_count != 1:
456
522
                raise BzrError("illegal LogRequest: must match-using-deltas "
457
 
                    "when logging %d files" % file_count)
 
523
                               "when logging %d files" % file_count)
458
524
            return self._log_revision_iterator_using_per_file_graph()
459
525
 
460
526
    def _log_revision_iterator_using_delta_matching(self):
462
528
        rqst = self.rqst
463
529
        generate_merge_revisions = rqst.get('levels') != 1
464
530
        delayed_graph_generation = not rqst.get('specific_fileids') and (
465
 
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
531
            rqst.get('limit') or self.start_rev_id or self.end_rev_id)
466
532
        view_revisions = _calc_view_revisions(
467
533
            self.branch, self.start_rev_id, self.end_rev_id,
468
534
            rqst.get('direction'),
472
538
 
473
539
        # Apply the other filters
474
540
        return make_log_rev_iterator(self.branch, view_revisions,
475
 
            rqst.get('delta_type'), rqst.get('message_search'),
476
 
            file_ids=rqst.get('specific_fileids'),
477
 
            direction=rqst.get('direction'))
 
541
                                     rqst.get('delta_type'), rqst.get('match'),
 
542
                                     file_ids=rqst.get('specific_fileids'),
 
543
                                     direction=rqst.get('direction'))
478
544
 
479
545
    def _log_revision_iterator_using_per_file_graph(self):
480
546
        # Get the base revisions, filtering by the revision range.
488
554
        if not isinstance(view_revisions, list):
489
555
            view_revisions = list(view_revisions)
490
556
        view_revisions = _filter_revisions_touching_file_id(self.branch,
491
 
            rqst.get('specific_fileids')[0], view_revisions,
492
 
            include_merges=rqst.get('levels') != 1)
 
557
                                                            rqst.get('specific_fileids')[
 
558
                                                                0], view_revisions,
 
559
                                                            include_merges=rqst.get('levels') != 1)
493
560
        return make_log_rev_iterator(self.branch, view_revisions,
494
 
            rqst.get('delta_type'), rqst.get('message_search'))
 
561
                                     rqst.get('delta_type'), rqst.get('match'))
495
562
 
496
563
 
497
564
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
505
572
             a list of the same tuples.
506
573
    """
507
574
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
508
 
        raise errors.BzrCommandError(
509
 
            '--exclude-common-ancestry requires two different revisions')
 
575
        raise errors.BzrCommandError(gettext(
 
576
            '--exclude-common-ancestry requires two different revisions'))
510
577
    if direction not in ('reverse', 'forward'):
511
 
        raise ValueError('invalid direction %r' % direction)
512
 
    br_revno, br_rev_id = branch.last_revision_info()
513
 
    if br_revno == 0:
 
578
        raise ValueError(gettext('invalid direction %r') % direction)
 
579
    br_rev_id = branch.last_revision()
 
580
    if br_rev_id == _mod_revision.NULL_REVISION:
514
581
        return []
515
582
 
516
583
    if (end_rev_id and start_rev_id == end_rev_id
517
584
        and (not generate_merge_revisions
518
 
             or not _has_merges(branch, end_rev_id))):
 
585
         or not _has_merges(branch, end_rev_id))):
519
586
        # If a single revision is requested, check we can handle it
520
 
        iter_revs = _generate_one_revision(branch, end_rev_id, br_rev_id,
521
 
                                           br_revno)
522
 
    elif not generate_merge_revisions:
523
 
        # If we only want to see linear revisions, we can iterate ...
524
 
        iter_revs = _generate_flat_revisions(branch, start_rev_id, end_rev_id,
525
 
                                             direction)
526
 
        if direction == 'forward':
527
 
            iter_revs = reversed(iter_revs)
528
 
    else:
529
 
        iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
530
 
                                            direction, delayed_graph_generation,
531
 
                                            exclude_common_ancestry)
532
 
        if direction == 'forward':
533
 
            iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
587
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
 
588
                                      branch.revno())
 
589
    if not generate_merge_revisions:
 
590
        try:
 
591
            # If we only want to see linear revisions, we can iterate ...
 
592
            iter_revs = _linear_view_revisions(
 
593
                branch, start_rev_id, end_rev_id,
 
594
                exclude_common_ancestry=exclude_common_ancestry)
 
595
            # If a start limit was given and it's not obviously an
 
596
            # ancestor of the end limit, check it before outputting anything
 
597
            if (direction == 'forward'
 
598
                or (start_rev_id and not _is_obvious_ancestor(
 
599
                    branch, start_rev_id, end_rev_id))):
 
600
                iter_revs = list(iter_revs)
 
601
            if direction == 'forward':
 
602
                iter_revs = reversed(iter_revs)
 
603
            return iter_revs
 
604
        except _StartNotLinearAncestor:
 
605
            # Switch to the slower implementation that may be able to find a
 
606
            # non-obvious ancestor out of the left-hand history.
 
607
            pass
 
608
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
609
                                        direction, delayed_graph_generation,
 
610
                                        exclude_common_ancestry)
 
611
    if direction == 'forward':
 
612
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
534
613
    return iter_revs
535
614
 
536
615
 
539
618
        # It's the tip
540
619
        return [(br_rev_id, br_revno, 0)]
541
620
    else:
542
 
        revno = branch.revision_id_to_dotted_revno(rev_id)
543
 
        revno_str = '.'.join(str(n) for n in revno)
 
621
        revno_str = _compute_revno_str(branch, rev_id)
544
622
        return [(rev_id, revno_str, 0)]
545
623
 
546
624
 
547
 
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
548
 
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
549
 
    # If a start limit was given and it's not obviously an
550
 
    # ancestor of the end limit, check it before outputting anything
551
 
    if direction == 'forward' or (start_rev_id
552
 
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
553
 
        try:
554
 
            result = list(result)
555
 
        except _StartNotLinearAncestor:
556
 
            raise errors.BzrCommandError('Start revision not found in'
557
 
                ' left-hand history of end revision.')
558
 
    return result
559
 
 
560
 
 
561
625
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
562
626
                            delayed_graph_generation,
563
627
                            exclude_common_ancestry=False):
571
635
    initial_revisions = []
572
636
    if delayed_graph_generation:
573
637
        try:
574
 
            for rev_id, revno, depth in  _linear_view_revisions(
575
 
                branch, start_rev_id, end_rev_id):
 
638
            for rev_id, revno, depth in _linear_view_revisions(
 
639
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
576
640
                if _has_merges(branch, rev_id):
577
641
                    # The end_rev_id can be nested down somewhere. We need an
578
642
                    # explicit ancestry check. There is an ambiguity here as we
585
649
                    # -- vila 20100319
586
650
                    graph = branch.repository.get_graph()
587
651
                    if (start_rev_id is not None
588
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
652
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
589
653
                        raise _StartNotLinearAncestor()
590
654
                    # Since we collected the revisions so far, we need to
591
655
                    # adjust end_rev_id.
599
663
        except _StartNotLinearAncestor:
600
664
            # A merge was never detected so the lower revision limit can't
601
665
            # be nested down somewhere
602
 
            raise errors.BzrCommandError('Start revision not found in'
603
 
                ' history of end revision.')
 
666
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
667
                                                 ' history of end revision.'))
604
668
 
605
669
    # We exit the loop above because we encounter a revision with merges, from
606
670
    # this revision, we need to switch to _graph_view_revisions.
610
674
    # shown naturally, i.e. just like it is for linear logging. We can easily
611
675
    # make forward the exact opposite display, but showing the merge revisions
612
676
    # indented at the end seems slightly nicer in that case.
613
 
    view_revisions = chain(iter(initial_revisions),
614
 
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
615
 
                              rebase_initial_depths=(direction == 'reverse'),
616
 
                              exclude_common_ancestry=exclude_common_ancestry))
 
677
    view_revisions = itertools.chain(iter(initial_revisions),
 
678
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
679
                                                           rebase_initial_depths=(
 
680
                                                               direction == 'reverse'),
 
681
                                                           exclude_common_ancestry=exclude_common_ancestry))
617
682
    return view_revisions
618
683
 
619
684
 
623
688
    return len(parents) > 1
624
689
 
625
690
 
 
691
def _compute_revno_str(branch, rev_id):
 
692
    """Compute the revno string from a rev_id.
 
693
 
 
694
    :return: The revno string, or None if the revision is not in the supplied
 
695
        branch.
 
696
    """
 
697
    try:
 
698
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
699
    except errors.NoSuchRevision:
 
700
        # The revision must be outside of this branch
 
701
        return None
 
702
    else:
 
703
        return '.'.join(str(n) for n in revno)
 
704
 
 
705
 
626
706
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
627
707
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
628
708
    if start_rev_id and end_rev_id:
629
 
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
630
 
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
709
        try:
 
710
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
711
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
712
        except errors.NoSuchRevision:
 
713
            # one or both is not in the branch; not obvious
 
714
            return False
631
715
        if len(start_dotted) == 1 and len(end_dotted) == 1:
632
716
            # both on mainline
633
717
            return start_dotted[0] <= end_dotted[0]
634
718
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
635
 
            start_dotted[0:1] == end_dotted[0:1]):
 
719
              start_dotted[0:1] == end_dotted[0:1]):
636
720
            # both on same development line
637
721
            return start_dotted[2] <= end_dotted[2]
638
722
        else:
643
727
    return True
644
728
 
645
729
 
646
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
 
730
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
731
                           exclude_common_ancestry=False):
647
732
    """Calculate a sequence of revisions to view, newest to oldest.
648
733
 
649
734
    :param start_rev_id: the lower revision-id
650
735
    :param end_rev_id: the upper revision-id
 
736
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
737
        the iterated revisions.
651
738
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
739
        dotted_revno will be None for ghosts
652
740
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
653
 
      is not found walking the left-hand history
 
741
        is not found walking the left-hand history
654
742
    """
655
 
    br_revno, br_rev_id = branch.last_revision_info()
656
743
    repo = branch.repository
 
744
    graph = repo.get_graph()
657
745
    if start_rev_id is None and end_rev_id is None:
658
 
        cur_revno = br_revno
659
 
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
660
 
            yield revision_id, str(cur_revno), 0
661
 
            cur_revno -= 1
 
746
        try:
 
747
            br_revno, br_rev_id = branch.last_revision_info()
 
748
        except errors.GhostRevisionsHaveNoRevno:
 
749
            br_rev_id = branch.last_revision()
 
750
            cur_revno = None
 
751
        else:
 
752
            cur_revno = br_revno
 
753
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
 
754
                                                  (_mod_revision.NULL_REVISION,))
 
755
        while True:
 
756
            try:
 
757
                revision_id = next(graph_iter)
 
758
            except errors.RevisionNotPresent as e:
 
759
                # Oops, a ghost.
 
760
                yield e.revision_id, None, None
 
761
                break
 
762
            except StopIteration:
 
763
                break
 
764
            else:
 
765
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
 
766
                if cur_revno is not None:
 
767
                    cur_revno -= 1
662
768
    else:
 
769
        br_rev_id = branch.last_revision()
663
770
        if end_rev_id is None:
664
771
            end_rev_id = br_rev_id
665
772
        found_start = start_rev_id is None
666
 
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
667
 
            revno = branch.revision_id_to_dotted_revno(revision_id)
668
 
            revno_str = '.'.join(str(n) for n in revno)
669
 
            if not found_start and revision_id == start_rev_id:
670
 
                yield revision_id, revno_str, 0
671
 
                found_start = True
 
773
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
 
774
                                                  (_mod_revision.NULL_REVISION,))
 
775
        while True:
 
776
            try:
 
777
                revision_id = next(graph_iter)
 
778
            except StopIteration:
 
779
                break
 
780
            except errors.RevisionNotPresent as e:
 
781
                # Oops, a ghost.
 
782
                yield e.revision_id, None, None
672
783
                break
673
784
            else:
674
 
                yield revision_id, revno_str, 0
675
 
        else:
676
 
            if not found_start:
677
 
                raise _StartNotLinearAncestor()
 
785
                revno_str = _compute_revno_str(branch, revision_id)
 
786
                if not found_start and revision_id == start_rev_id:
 
787
                    if not exclude_common_ancestry:
 
788
                        yield revision_id, revno_str, 0
 
789
                    found_start = True
 
790
                    break
 
791
                else:
 
792
                    yield revision_id, revno_str, 0
 
793
        if not found_start:
 
794
            raise _StartNotLinearAncestor()
678
795
 
679
796
 
680
797
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
721
838
            yield rev_id, '.'.join(map(str, revno)), merge_depth
722
839
 
723
840
 
724
 
@deprecated_function(deprecated_in((2, 2, 0)))
725
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
726
 
        specific_fileid, generate_merge_revisions):
727
 
    """Calculate the revisions to view.
728
 
 
729
 
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
730
 
             a list of the same tuples.
731
 
    """
732
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
733
 
        end_revision)
734
 
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
735
 
        direction, generate_merge_revisions or specific_fileid))
736
 
    if specific_fileid:
737
 
        view_revisions = _filter_revisions_touching_file_id(branch,
738
 
            specific_fileid, view_revisions,
739
 
            include_merges=generate_merge_revisions)
740
 
    return _rebase_merge_depth(view_revisions)
741
 
 
742
 
 
743
841
def _rebase_merge_depth(view_revisions):
744
842
    """Adjust depths upwards so the top level is 0."""
745
843
    # If either the first or last revision have a merge_depth of 0, we're done
746
844
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
747
 
        min_depth = min([d for r,n,d in view_revisions])
 
845
        min_depth = min([d for r, n, d in view_revisions])
748
846
        if min_depth != 0:
749
 
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
 
847
            view_revisions = [(r, n, d - min_depth)
 
848
                              for r, n, d in view_revisions]
750
849
    return view_revisions
751
850
 
752
851
 
753
852
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
754
 
        file_ids=None, direction='reverse'):
 
853
                          file_ids=None, direction='reverse'):
755
854
    """Create a revision iterator for log.
756
855
 
757
856
    :param branch: The branch being logged.
767
866
    """
768
867
    # Convert view_revisions into (view, None, None) groups to fit with
769
868
    # the standard interface here.
770
 
    if type(view_revisions) == list:
 
869
    if isinstance(view_revisions, list):
771
870
        # A single batch conversion is faster than many incremental ones.
772
871
        # As we have all the data, do a batch conversion.
773
872
        nones = [None] * len(view_revisions)
774
 
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
 
873
        log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
775
874
    else:
776
875
        def _convert():
777
876
            for view in view_revisions:
782
881
        # with custom parameters. This will do for now. IGC 20090127
783
882
        if adapter == _make_delta_filter:
784
883
            log_rev_iterator = adapter(branch, generate_delta,
785
 
                search, log_rev_iterator, file_ids, direction)
 
884
                                       search, log_rev_iterator, file_ids, direction)
786
885
        else:
787
886
            log_rev_iterator = adapter(branch, generate_delta,
788
 
                search, log_rev_iterator)
 
887
                                       search, log_rev_iterator)
789
888
    return log_rev_iterator
790
889
 
791
890
 
792
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
891
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
793
892
    """Create a filtered iterator of log_rev_iterator matching on a regex.
794
893
 
795
894
    :param branch: The branch being logged.
796
895
    :param generate_delta: Whether to generate a delta for each revision.
797
 
    :param search: A user text search string.
 
896
    :param match: A dictionary with properties as keys and lists of strings
 
897
        as values. To match, a revision may match any of the supplied strings
 
898
        within a single property but must match at least one string for each
 
899
        property.
798
900
    :param log_rev_iterator: An input iterator containing all revisions that
799
901
        could be displayed, in lists.
800
902
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
801
903
        delta).
802
904
    """
803
 
    if search is None:
 
905
    if not match:
804
906
        return log_rev_iterator
805
 
    searchRE = re_compile_checked(search, re.IGNORECASE,
806
 
            'log message filter')
807
 
    return _filter_message_re(searchRE, log_rev_iterator)
808
 
 
809
 
 
810
 
def _filter_message_re(searchRE, log_rev_iterator):
 
907
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
908
                for k, v in match.items()]
 
909
    return _filter_re(searchRE, log_rev_iterator)
 
910
 
 
911
 
 
912
def _filter_re(searchRE, log_rev_iterator):
811
913
    for revs in log_rev_iterator:
812
 
        new_revs = []
813
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
814
 
            if searchRE.search(rev.message):
815
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
816
 
        yield new_revs
 
914
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
915
        if new_revs:
 
916
            yield new_revs
 
917
 
 
918
 
 
919
def _match_filter(searchRE, rev):
 
920
    strings = {
 
921
        'message': (rev.message,),
 
922
        'committer': (rev.committer,),
 
923
        'author': (rev.get_apparent_authors()),
 
924
        'bugs': list(rev.iter_bugs())
 
925
        }
 
926
    strings[''] = [item for inner_list in strings.values()
 
927
                   for item in inner_list]
 
928
    for (k, v) in searchRE:
 
929
        if k in strings and not _match_any_filter(strings[k], v):
 
930
            return False
 
931
    return True
 
932
 
 
933
 
 
934
def _match_any_filter(strings, res):
 
935
    return any(re.search(s) for re in res for s in strings)
817
936
 
818
937
 
819
938
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
820
 
    fileids=None, direction='reverse'):
 
939
                       fileids=None, direction='reverse'):
821
940
    """Add revision deltas to a log iterator if needed.
822
941
 
823
942
    :param branch: The branch being logged.
835
954
    if not generate_delta and not fileids:
836
955
        return log_rev_iterator
837
956
    return _generate_deltas(branch.repository, log_rev_iterator,
838
 
        generate_delta, fileids, direction)
 
957
                            generate_delta, fileids, direction)
839
958
 
840
959
 
841
960
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
842
 
    direction):
 
961
                     direction):
843
962
    """Create deltas for each batch of revisions in log_rev_iterator.
844
963
 
845
964
    If we're only generating deltas for the sake of filtering against
865
984
        new_revs = []
866
985
        if delta_type == 'full' and not check_fileids:
867
986
            deltas = repository.get_deltas_for_revisions(revisions)
868
 
            for rev, delta in izip(revs, deltas):
 
987
            for rev, delta in zip(revs, deltas):
869
988
                new_revs.append((rev[0], rev[1], delta))
870
989
        else:
871
990
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
872
 
            for rev, delta in izip(revs, deltas):
 
991
            for rev, delta in zip(revs, deltas):
873
992
                if check_fileids:
874
993
                    if delta is None or not delta.has_changed():
875
994
                        continue
893
1012
 
894
1013
def _update_fileids(delta, fileids, stop_on):
895
1014
    """Update the set of file-ids to search based on file lifecycle events.
896
 
    
 
1015
 
897
1016
    :param fileids: a set of fileids to update
898
1017
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
899
1018
      fileids set once their add or remove entry is detected respectively
923
1042
    for revs in log_rev_iterator:
924
1043
        # r = revision_id, n = revno, d = merge depth
925
1044
        revision_ids = [view[0] for view, _, _ in revs]
926
 
        revisions = repository.get_revisions(revision_ids)
927
 
        revs = [(rev[0], revision, rev[2]) for rev, revision in
928
 
            izip(revs, revisions)]
929
 
        yield revs
 
1045
        revisions = dict(repository.iter_revisions(revision_ids))
 
1046
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
930
1047
 
931
1048
 
932
1049
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
940
1057
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
941
1058
        delta).
942
1059
    """
943
 
    repository = branch.repository
944
1060
    num = 9
945
1061
    for batch in log_rev_iterator:
946
1062
        batch = iter(batch)
958
1074
    :param  branch: The branch containing the revisions.
959
1075
 
960
1076
    :param  start_revision: The first revision to be logged.
961
 
            For backwards compatibility this may be a mainline integer revno,
962
1077
            but for merge revision support a RevisionInfo is expected.
963
1078
 
964
1079
    :param  end_revision: The last revision to be logged.
967
1082
 
968
1083
    :return: (start_rev_id, end_rev_id) tuple.
969
1084
    """
970
 
    branch_revno, branch_rev_id = branch.last_revision_info()
971
1085
    start_rev_id = None
972
 
    if start_revision is None:
 
1086
    start_revno = None
 
1087
    if start_revision is not None:
 
1088
        if not isinstance(start_revision, revisionspec.RevisionInfo):
 
1089
            raise TypeError(start_revision)
 
1090
        start_rev_id = start_revision.rev_id
 
1091
        start_revno = start_revision.revno
 
1092
    if start_revno is None:
973
1093
        start_revno = 1
974
 
    else:
975
 
        if isinstance(start_revision, revisionspec.RevisionInfo):
976
 
            start_rev_id = start_revision.rev_id
977
 
            start_revno = start_revision.revno or 1
978
 
        else:
979
 
            branch.check_real_revno(start_revision)
980
 
            start_revno = start_revision
981
 
            start_rev_id = branch.get_rev_id(start_revno)
982
1094
 
983
1095
    end_rev_id = None
984
 
    if end_revision is None:
985
 
        end_revno = branch_revno
986
 
    else:
987
 
        if isinstance(end_revision, revisionspec.RevisionInfo):
988
 
            end_rev_id = end_revision.rev_id
989
 
            end_revno = end_revision.revno or branch_revno
990
 
        else:
991
 
            branch.check_real_revno(end_revision)
992
 
            end_revno = end_revision
993
 
            end_rev_id = branch.get_rev_id(end_revno)
 
1096
    end_revno = None
 
1097
    if end_revision is not None:
 
1098
        if not isinstance(end_revision, revisionspec.RevisionInfo):
 
1099
            raise TypeError(start_revision)
 
1100
        end_rev_id = end_revision.rev_id
 
1101
        end_revno = end_revision.revno
 
1102
    if end_revno is None:
 
1103
        try:
 
1104
            end_revno = branch.revno()
 
1105
        except errors.GhostRevisionsHaveNoRevno:
 
1106
            end_revno = None
994
1107
 
995
 
    if branch_revno != 0:
 
1108
    if branch.last_revision() != _mod_revision.NULL_REVISION:
996
1109
        if (start_rev_id == _mod_revision.NULL_REVISION
997
 
            or end_rev_id == _mod_revision.NULL_REVISION):
998
 
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
999
 
        if start_revno > end_revno:
1000
 
            raise errors.BzrCommandError("Start revision must be older than "
1001
 
                                         "the end revision.")
 
1110
                or end_rev_id == _mod_revision.NULL_REVISION):
 
1111
            raise errors.BzrCommandError(
 
1112
                gettext('Logging revision 0 is invalid.'))
 
1113
        if end_revno is not None and start_revno > end_revno:
 
1114
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1115
                                                 "older than the end revision."))
1002
1116
    return (start_rev_id, end_rev_id)
1003
1117
 
1004
1118
 
1052
1166
            end_revno = end_revision
1053
1167
 
1054
1168
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1055
 
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1056
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
1169
            or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1170
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1057
1171
    if start_revno > end_revno:
1058
 
        raise errors.BzrCommandError("Start revision must be older than "
1059
 
                                     "the end revision.")
 
1172
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1173
                                             "than the end revision."))
1060
1174
 
1061
1175
    if end_revno < start_revno:
1062
1176
        return None, None, None, None
1063
1177
    cur_revno = branch_revno
1064
1178
    rev_nos = {}
1065
1179
    mainline_revs = []
1066
 
    for revision_id in branch.repository.iter_reverse_revision_history(
1067
 
                        branch_last_revision):
 
1180
    graph = branch.repository.get_graph()
 
1181
    for revision_id in graph.iter_lefthand_ancestry(
 
1182
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
1068
1183
        if cur_revno < start_revno:
1069
1184
            # We have gone far enough, but we always add 1 more revision
1070
1185
            rev_nos[revision_id] = cur_revno
1084
1199
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1085
1200
 
1086
1201
 
1087
 
@deprecated_function(deprecated_in((2, 2, 0)))
1088
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
1089
 
    """Filter view_revisions based on revision ranges.
1090
 
 
1091
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1092
 
            tuples to be filtered.
1093
 
 
1094
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
1095
 
            If NONE then all revisions up to the end_rev_id are logged.
1096
 
 
1097
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
1098
 
            If NONE then all revisions up to the end of the log are logged.
1099
 
 
1100
 
    :return: The filtered view_revisions.
1101
 
    """
1102
 
    if start_rev_id or end_rev_id:
1103
 
        revision_ids = [r for r, n, d in view_revisions]
1104
 
        if start_rev_id:
1105
 
            start_index = revision_ids.index(start_rev_id)
1106
 
        else:
1107
 
            start_index = 0
1108
 
        if start_rev_id == end_rev_id:
1109
 
            end_index = start_index
1110
 
        else:
1111
 
            if end_rev_id:
1112
 
                end_index = revision_ids.index(end_rev_id)
1113
 
            else:
1114
 
                end_index = len(view_revisions) - 1
1115
 
        # To include the revisions merged into the last revision,
1116
 
        # extend end_rev_id down to, but not including, the next rev
1117
 
        # with the same or lesser merge_depth
1118
 
        end_merge_depth = view_revisions[end_index][2]
1119
 
        try:
1120
 
            for index in xrange(end_index+1, len(view_revisions)+1):
1121
 
                if view_revisions[index][2] <= end_merge_depth:
1122
 
                    end_index = index - 1
1123
 
                    break
1124
 
        except IndexError:
1125
 
            # if the search falls off the end then log to the end as well
1126
 
            end_index = len(view_revisions) - 1
1127
 
        view_revisions = view_revisions[start_index:end_index+1]
1128
 
    return view_revisions
1129
 
 
1130
 
 
1131
1202
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1132
 
    include_merges=True):
 
1203
                                       include_merges=True):
1133
1204
    r"""Return the list of revision ids which touch a given file id.
1134
1205
 
1135
1206
    The function filters view_revisions and returns a subset.
1136
1207
    This includes the revisions which directly change the file id,
1137
1208
    and the revisions which merge these changes. So if the
1138
1209
    revision graph is::
 
1210
 
1139
1211
        A-.
1140
1212
        |\ \
1141
1213
        B C E
1168
1240
    """
1169
1241
    # Lookup all possible text keys to determine which ones actually modified
1170
1242
    # the file.
 
1243
    graph = branch.repository.get_file_graph()
 
1244
    get_parent_map = graph.get_parent_map
1171
1245
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1172
1246
    next_keys = None
1173
1247
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1177
1251
    #       indexing layer. We might consider passing in hints as to the known
1178
1252
    #       access pattern (sparse/clustered, high success rate/low success
1179
1253
    #       rate). This particular access is clustered with a low success rate.
1180
 
    get_parent_map = branch.repository.texts.get_parent_map
1181
1254
    modified_text_revisions = set()
1182
1255
    chunk_size = 1000
1183
 
    for start in xrange(0, len(text_keys), chunk_size):
 
1256
    for start in range(0, len(text_keys), chunk_size):
1184
1257
        next_keys = text_keys[start:start + chunk_size]
1185
1258
        # Only keep the revision_id portion of the key
1186
1259
        modified_text_revisions.update(
1201
1274
 
1202
1275
        if rev_id in modified_text_revisions:
1203
1276
            # This needs to be logged, along with the extra revisions
1204
 
            for idx in xrange(len(current_merge_stack)):
 
1277
            for idx in range(len(current_merge_stack)):
1205
1278
                node = current_merge_stack[idx]
1206
1279
                if node is not None:
1207
1280
                    if include_merges or node[2] == 0:
1210
1283
    return result
1211
1284
 
1212
1285
 
1213
 
@deprecated_function(deprecated_in((2, 2, 0)))
1214
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
1215
 
                       include_merges=True):
1216
 
    """Produce an iterator of revisions to show
1217
 
    :return: an iterator of (revision_id, revno, merge_depth)
1218
 
    (if there is no revno for a revision, None is supplied)
1219
 
    """
1220
 
    if not include_merges:
1221
 
        revision_ids = mainline_revs[1:]
1222
 
        if direction == 'reverse':
1223
 
            revision_ids.reverse()
1224
 
        for revision_id in revision_ids:
1225
 
            yield revision_id, str(rev_nos[revision_id]), 0
1226
 
        return
1227
 
    graph = branch.repository.get_graph()
1228
 
    # This asks for all mainline revisions, which means we only have to spider
1229
 
    # sideways, rather than depth history. That said, its still size-of-history
1230
 
    # and should be addressed.
1231
 
    # mainline_revisions always includes an extra revision at the beginning, so
1232
 
    # don't request it.
1233
 
    parent_map = dict(((key, value) for key, value in
1234
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
1235
 
    # filter out ghosts; merge_sort errors on ghosts.
1236
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
1237
 
    merge_sorted_revisions = tsort.merge_sort(
1238
 
        rev_graph,
1239
 
        mainline_revs[-1],
1240
 
        mainline_revs,
1241
 
        generate_revno=True)
1242
 
 
1243
 
    if direction == 'forward':
1244
 
        # forward means oldest first.
1245
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
1246
 
    elif direction != 'reverse':
1247
 
        raise ValueError('invalid direction %r' % direction)
1248
 
 
1249
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
1250
 
         ) in merge_sorted_revisions:
1251
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
1252
 
 
1253
 
 
1254
1286
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1255
1287
    """Reverse revisions by depth.
1256
1288
 
1291
1323
    """
1292
1324
 
1293
1325
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1294
 
                 tags=None, diff=None):
 
1326
                 tags=None, diff=None, signature=None):
1295
1327
        self.rev = rev
1296
 
        self.revno = str(revno)
 
1328
        if revno is None:
 
1329
            self.revno = None
 
1330
        else:
 
1331
            self.revno = str(revno)
1297
1332
        self.merge_depth = merge_depth
1298
1333
        self.delta = delta
1299
1334
        self.tags = tags
1300
1335
        self.diff = diff
 
1336
        self.signature = signature
1301
1337
 
1302
1338
 
1303
1339
class LogFormatter(object):
1312
1348
    to indicate which LogRevision attributes it supports:
1313
1349
 
1314
1350
    - supports_delta must be True if this log formatter supports delta.
1315
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
1316
 
        attribute describes whether the 'short_status' format (1) or the long
1317
 
        one (2) should be used.
 
1351
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1352
      attribute describes whether the 'short_status' format (1) or the long
 
1353
      one (2) should be used.
1318
1354
 
1319
1355
    - supports_merge_revisions must be True if this log formatter supports
1320
 
        merge revisions.  If not, then only mainline revisions will be passed
1321
 
        to the formatter.
 
1356
      merge revisions.  If not, then only mainline revisions will be passed
 
1357
      to the formatter.
1322
1358
 
1323
1359
    - preferred_levels is the number of levels this formatter defaults to.
1324
 
        The default value is zero meaning display all levels.
1325
 
        This value is only relevant if supports_merge_revisions is True.
 
1360
      The default value is zero meaning display all levels.
 
1361
      This value is only relevant if supports_merge_revisions is True.
1326
1362
 
1327
1363
    - supports_tags must be True if this log formatter supports tags.
1328
 
        Otherwise the tags attribute may not be populated.
 
1364
      Otherwise the tags attribute may not be populated.
1329
1365
 
1330
1366
    - supports_diff must be True if this log formatter supports diffs.
1331
 
        Otherwise the diff attribute may not be populated.
 
1367
      Otherwise the diff attribute may not be populated.
 
1368
 
 
1369
    - supports_signatures must be True if this log formatter supports GPG
 
1370
      signatures.
1332
1371
 
1333
1372
    Plugins can register functions to show custom revision properties using
1334
1373
    the properties_handler_registry. The registered function
1335
 
    must respect the following interface description:
 
1374
    must respect the following interface description::
 
1375
 
1336
1376
        def my_show_properties(properties_dict):
1337
1377
            # code that returns a dict {'name':'value'} of the properties
1338
1378
            # to be shown
1341
1381
 
1342
1382
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1343
1383
                 delta_format=None, levels=None, show_advice=False,
1344
 
                 to_exact_file=None):
 
1384
                 to_exact_file=None, author_list_handler=None):
1345
1385
        """Create a LogFormatter.
1346
1386
 
1347
1387
        :param to_file: the file to output to
1348
 
        :param to_exact_file: if set, gives an output stream to which 
 
1388
        :param to_exact_file: if set, gives an output stream to which
1349
1389
             non-Unicode diffs are written.
1350
1390
        :param show_ids: if True, revision-ids are to be displayed
1351
1391
        :param show_timezone: the timezone to use
1355
1395
          let the log formatter decide.
1356
1396
        :param show_advice: whether to show advice at the end of the
1357
1397
          log or not
 
1398
        :param author_list_handler: callable generating a list of
 
1399
          authors to display for a given revision
1358
1400
        """
1359
1401
        self.to_file = to_file
1360
1402
        # 'exact' stream used to show diff, it should print content 'as is'
1370
1412
        self.show_timezone = show_timezone
1371
1413
        if delta_format is None:
1372
1414
            # Ensures backward compatibility
1373
 
            delta_format = 2 # long format
 
1415
            delta_format = 2  # long format
1374
1416
        self.delta_format = delta_format
1375
1417
        self.levels = levels
1376
1418
        self._show_advice = show_advice
1377
1419
        self._merge_count = 0
 
1420
        self._author_list_handler = author_list_handler
1378
1421
 
1379
1422
    def get_levels(self):
1380
1423
        """Get the number of levels to display or 0 for all."""
1399
1442
            if advice_sep:
1400
1443
                self.to_file.write(advice_sep)
1401
1444
            self.to_file.write(
1402
 
                "Use --include-merges or -n0 to see merged revisions.\n")
 
1445
                "Use --include-merged or -n0 to see merged revisions.\n")
1403
1446
 
1404
1447
    def get_advice_separator(self):
1405
1448
        """Get the text separating the log from the closing advice."""
1412
1455
        return address
1413
1456
 
1414
1457
    def short_author(self, rev):
1415
 
        name, address = config.parse_username(rev.get_apparent_authors()[0])
1416
 
        if name:
1417
 
            return name
1418
 
        return address
 
1458
        return self.authors(rev, 'first', short=True, sep=', ')
 
1459
 
 
1460
    def authors(self, rev, who, short=False, sep=None):
 
1461
        """Generate list of authors, taking --authors option into account.
 
1462
 
 
1463
        The caller has to specify the name of a author list handler,
 
1464
        as provided by the author list registry, using the ``who``
 
1465
        argument.  That name only sets a default, though: when the
 
1466
        user selected a different author list generation using the
 
1467
        ``--authors`` command line switch, as represented by the
 
1468
        ``author_list_handler`` constructor argument, that value takes
 
1469
        precedence.
 
1470
 
 
1471
        :param rev: The revision for which to generate the list of authors.
 
1472
        :param who: Name of the default handler.
 
1473
        :param short: Whether to shorten names to either name or address.
 
1474
        :param sep: What separator to use for automatic concatenation.
 
1475
        """
 
1476
        if self._author_list_handler is not None:
 
1477
            # The user did specify --authors, which overrides the default
 
1478
            author_list_handler = self._author_list_handler
 
1479
        else:
 
1480
            # The user didn't specify --authors, so we use the caller's default
 
1481
            author_list_handler = author_list_registry.get(who)
 
1482
        names = author_list_handler(rev)
 
1483
        if short:
 
1484
            for i in range(len(names)):
 
1485
                name, address = config.parse_username(names[i])
 
1486
                if name:
 
1487
                    names[i] = name
 
1488
                else:
 
1489
                    names[i] = address
 
1490
        if sep is not None:
 
1491
            names = sep.join(names)
 
1492
        return names
1419
1493
 
1420
1494
    def merge_marker(self, revision):
1421
1495
        """Get the merge marker to include in the output or '' if none."""
1456
1530
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1457
1531
 
1458
1532
        # Imported foreign revision revision ids always contain :
1459
 
        if not ":" in rev.revision_id:
 
1533
        if not b":" in rev.revision_id:
1460
1534
            return []
1461
1535
 
1462
1536
        # Revision was once imported from a foreign repository
1476
1550
        return lines
1477
1551
 
1478
1552
    def show_diff(self, to_file, diff, indent):
1479
 
        for l in diff.rstrip().split('\n'):
1480
 
            to_file.write(indent + '%s\n' % (l,))
 
1553
        encoding = get_terminal_encoding()
 
1554
        for l in diff.rstrip().split(b'\n'):
 
1555
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
1481
1556
 
1482
1557
 
1483
1558
# Separator between revisions in long format
1491
1566
    supports_delta = True
1492
1567
    supports_tags = True
1493
1568
    supports_diff = True
 
1569
    supports_signatures = True
1494
1570
 
1495
1571
    def __init__(self, *args, **kwargs):
1496
1572
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1505
1581
 
1506
1582
    def _date_string_original_timezone(self, rev):
1507
1583
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1508
 
            rev.timezone or 0)
 
1584
                                                            rev.timezone or 0)
1509
1585
 
1510
1586
    def log_revision(self, revision):
1511
1587
        """Log a revision, either merged or not."""
1513
1589
        lines = [_LONG_SEP]
1514
1590
        if revision.revno is not None:
1515
1591
            lines.append('revno: %s%s' % (revision.revno,
1516
 
                self.merge_marker(revision)))
 
1592
                                          self.merge_marker(revision)))
1517
1593
        if revision.tags:
1518
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1594
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1595
        if self.show_ids or revision.revno is None:
 
1596
            lines.append('revision-id: %s' %
 
1597
                         (revision.rev.revision_id.decode('utf-8'),))
1519
1598
        if self.show_ids:
1520
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1521
1599
            for parent_id in revision.rev.parent_ids:
1522
 
                lines.append('parent: %s' % (parent_id,))
 
1600
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1523
1601
        lines.extend(self.custom_properties(revision.rev))
1524
1602
 
1525
1603
        committer = revision.rev.committer
1526
 
        authors = revision.rev.get_apparent_authors()
 
1604
        authors = self.authors(revision.rev, 'all')
1527
1605
        if authors != [committer]:
1528
1606
            lines.append('author: %s' % (", ".join(authors),))
1529
1607
        lines.append('committer: %s' % (committer,))
1534
1612
 
1535
1613
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1536
1614
 
 
1615
        if revision.signature is not None:
 
1616
            lines.append('signature: ' + revision.signature)
 
1617
 
1537
1618
        lines.append('message:')
1538
1619
        if not revision.rev.message:
1539
1620
            lines.append('  (no message)')
1547
1628
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1548
1629
        if revision.delta is not None:
1549
1630
            # Use the standard status output to display changes
1550
 
            from bzrlib.delta import report_delta
1551
 
            report_delta(to_file, revision.delta, short_status=False, 
 
1631
            from breezy.delta import report_delta
 
1632
            report_delta(to_file, revision.delta, short_status=False,
1552
1633
                         show_ids=self.show_ids, indent=indent)
1553
1634
        if revision.diff is not None:
1554
1635
            to_file.write(indent + 'diff:\n')
1586
1667
        indent = '    ' * depth
1587
1668
        revno_width = self.revno_width_by_depth.get(depth)
1588
1669
        if revno_width is None:
1589
 
            if revision.revno.find('.') == -1:
 
1670
            if revision.revno is None or revision.revno.find('.') == -1:
1590
1671
                # mainline revno, e.g. 12345
1591
1672
                revno_width = 5
1592
1673
            else:
1598
1679
        to_file = self.to_file
1599
1680
        tags = ''
1600
1681
        if revision.tags:
1601
 
            tags = ' {%s}' % (', '.join(revision.tags))
 
1682
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1602
1683
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1603
 
                revision.revno, self.short_author(revision.rev),
1604
 
                format_date(revision.rev.timestamp,
1605
 
                            revision.rev.timezone or 0,
1606
 
                            self.show_timezone, date_fmt="%Y-%m-%d",
1607
 
                            show_offset=False),
1608
 
                tags, self.merge_marker(revision)))
1609
 
        self.show_properties(revision.rev, indent+offset)
1610
 
        if self.show_ids:
 
1684
                                                     revision.revno or "", self.short_author(
 
1685
                                                         revision.rev),
 
1686
                                                     format_date(revision.rev.timestamp,
 
1687
                                                                 revision.rev.timezone or 0,
 
1688
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
 
1689
                                                                 show_offset=False),
 
1690
                                                     tags, self.merge_marker(revision)))
 
1691
        self.show_properties(revision.rev, indent + offset)
 
1692
        if self.show_ids or revision.revno is None:
1611
1693
            to_file.write(indent + offset + 'revision-id:%s\n'
1612
 
                          % (revision.rev.revision_id,))
 
1694
                          % (revision.rev.revision_id.decode('utf-8'),))
1613
1695
        if not revision.rev.message:
1614
1696
            to_file.write(indent + offset + '(no message)\n')
1615
1697
        else:
1619
1701
 
1620
1702
        if revision.delta is not None:
1621
1703
            # Use the standard status output to display changes
1622
 
            from bzrlib.delta import report_delta
1623
 
            report_delta(to_file, revision.delta, 
1624
 
                         short_status=self.delta_format==1, 
 
1704
            from breezy.delta import report_delta
 
1705
            report_delta(to_file, revision.delta,
 
1706
                         short_status=self.delta_format == 1,
1625
1707
                         show_ids=self.show_ids, indent=indent + offset)
1626
1708
        if revision.diff is not None:
1627
1709
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1645
1727
    def truncate(self, str, max_len):
1646
1728
        if max_len is None or len(str) <= max_len:
1647
1729
            return str
1648
 
        return str[:max_len-3] + '...'
 
1730
        return str[:max_len - 3] + '...'
1649
1731
 
1650
1732
    def date_string(self, rev):
1651
1733
        return format_date(rev.timestamp, rev.timezone or 0,
1661
1743
    def log_revision(self, revision):
1662
1744
        indent = '  ' * revision.merge_depth
1663
1745
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1664
 
            self._max_chars, revision.tags, indent))
 
1746
                                           self._max_chars, revision.tags, indent))
1665
1747
        self.to_file.write('\n')
1666
1748
 
1667
1749
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1668
1750
        """Format log info into one string. Truncate tail of string
1669
 
        :param  revno:      revision number or None.
1670
 
                            Revision numbers counts from 1.
1671
 
        :param  rev:        revision object
1672
 
        :param  max_chars:  maximum length of resulting string
1673
 
        :param  tags:       list of tags or None
1674
 
        :param  prefix:     string to prefix each line
1675
 
        :return:            formatted truncated string
 
1751
 
 
1752
        :param revno:      revision number or None.
 
1753
                           Revision numbers counts from 1.
 
1754
        :param rev:        revision object
 
1755
        :param max_chars:  maximum length of resulting string
 
1756
        :param tags:       list of tags or None
 
1757
        :param prefix:     string to prefix each line
 
1758
        :return:           formatted truncated string
1676
1759
        """
1677
1760
        out = []
1678
1761
        if revno:
1679
1762
            # show revno only when is not None
1680
1763
            out.append("%s:" % revno)
1681
 
        out.append(self.truncate(self.short_author(rev), 20))
 
1764
        if max_chars is not None:
 
1765
            out.append(self.truncate(
 
1766
                self.short_author(rev), (max_chars + 3) // 4))
 
1767
        else:
 
1768
            out.append(self.short_author(rev))
1682
1769
        out.append(self.date_string(rev))
1683
1770
        if len(rev.parent_ids) > 1:
1684
1771
            out.append('[merge]')
1685
1772
        if tags:
1686
 
            tag_str = '{%s}' % (', '.join(tags))
 
1773
            tag_str = '{%s}' % (', '.join(sorted(tags)))
1687
1774
            out.append(tag_str)
1688
1775
        out.append(rev.get_summary())
1689
1776
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1703
1790
                               self.show_timezone,
1704
1791
                               date_fmt='%Y-%m-%d',
1705
1792
                               show_offset=False)
1706
 
        committer_str = revision.rev.get_apparent_authors()[0].replace (' <', '  <')
1707
 
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
 
1793
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1794
        committer_str = committer_str.replace(' <', '  <')
 
1795
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
1708
1796
 
1709
1797
        if revision.delta is not None and revision.delta.has_changed():
1710
1798
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1711
1799
                path, = c[:1]
1712
1800
                to_file.write('\t* %s:\n' % (path,))
1713
1801
            for c in revision.delta.renamed:
1714
 
                oldpath,newpath = c[:2]
 
1802
                oldpath, newpath = c[:2]
1715
1803
                # For renamed files, show both the old and the new path
1716
 
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
 
1804
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
1717
1805
            to_file.write('\n')
1718
1806
 
1719
1807
        if not revision.rev.message:
1742
1830
        return self.get(name)(*args, **kwargs)
1743
1831
 
1744
1832
    def get_default(self, branch):
1745
 
        return self.get(branch.get_config().log_format())
 
1833
        c = branch.get_config_stack()
 
1834
        return self.get(c.get('log_format'))
1746
1835
 
1747
1836
 
1748
1837
log_formatter_registry = LogFormatterRegistry()
1749
1838
 
1750
1839
 
1751
1840
log_formatter_registry.register('short', ShortLogFormatter,
1752
 
                                'Moderately short log format')
 
1841
                                'Moderately short log format.')
1753
1842
log_formatter_registry.register('long', LongLogFormatter,
1754
 
                                'Detailed log format')
 
1843
                                'Detailed log format.')
1755
1844
log_formatter_registry.register('line', LineLogFormatter,
1756
 
                                'Log format with one line per revision')
 
1845
                                'Log format with one line per revision.')
1757
1846
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1758
 
                                'Format used by GNU ChangeLog files')
 
1847
                                'Format used by GNU ChangeLog files.')
1759
1848
 
1760
1849
 
1761
1850
def register_formatter(name, formatter):
1771
1860
    try:
1772
1861
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1773
1862
    except KeyError:
1774
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1775
 
 
1776
 
 
1777
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1778
 
    # deprecated; for compatibility
1779
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1780
 
    lf.show(revno, rev, delta)
 
1863
        raise errors.BzrCommandError(
 
1864
            gettext("unknown log formatter: %r") % name)
 
1865
 
 
1866
 
 
1867
def author_list_all(rev):
 
1868
    return rev.get_apparent_authors()[:]
 
1869
 
 
1870
 
 
1871
def author_list_first(rev):
 
1872
    lst = rev.get_apparent_authors()
 
1873
    try:
 
1874
        return [lst[0]]
 
1875
    except IndexError:
 
1876
        return []
 
1877
 
 
1878
 
 
1879
def author_list_committer(rev):
 
1880
    return [rev.committer]
 
1881
 
 
1882
 
 
1883
author_list_registry = registry.Registry()
 
1884
 
 
1885
author_list_registry.register('all', author_list_all,
 
1886
                              'All authors')
 
1887
 
 
1888
author_list_registry.register('first', author_list_first,
 
1889
                              'The first author')
 
1890
 
 
1891
author_list_registry.register('committer', author_list_committer,
 
1892
                              'The committer')
1781
1893
 
1782
1894
 
1783
1895
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1791
1903
    """
1792
1904
    if to_file is None:
1793
1905
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1794
 
            errors='replace')
 
1906
                                                            errors='replace')
1795
1907
    lf = log_formatter(log_format,
1796
1908
                       show_ids=False,
1797
1909
                       to_file=to_file,
1800
1912
    # This is the first index which is different between
1801
1913
    # old and new
1802
1914
    base_idx = None
1803
 
    for i in xrange(max(len(new_rh),
1804
 
                        len(old_rh))):
 
1915
    for i in range(max(len(new_rh), len(old_rh))):
1805
1916
        if (len(new_rh) <= i
1806
1917
            or len(old_rh) <= i
1807
 
            or new_rh[i] != old_rh[i]):
 
1918
                or new_rh[i] != old_rh[i]):
1808
1919
            base_idx = i
1809
1920
            break
1810
1921
 
1811
1922
    if base_idx is None:
1812
1923
        to_file.write('Nothing seems to have changed\n')
1813
1924
        return
1814
 
    ## TODO: It might be nice to do something like show_log
1815
 
    ##       and show the merged entries. But since this is the
1816
 
    ##       removed revisions, it shouldn't be as important
 
1925
    # TODO: It might be nice to do something like show_log
 
1926
    # and show the merged entries. But since this is the
 
1927
    # removed revisions, it shouldn't be as important
1817
1928
    if base_idx < len(old_rh):
1818
 
        to_file.write('*'*60)
 
1929
        to_file.write('*' * 60)
1819
1930
        to_file.write('\nRemoved Revisions:\n')
1820
1931
        for i in range(base_idx, len(old_rh)):
1821
1932
            rev = branch.repository.get_revision(old_rh[i])
1822
 
            lr = LogRevision(rev, i+1, 0, None)
 
1933
            lr = LogRevision(rev, i + 1, 0, None)
1823
1934
            lf.log_revision(lr)
1824
 
        to_file.write('*'*60)
 
1935
        to_file.write('*' * 60)
1825
1936
        to_file.write('\n\n')
1826
1937
    if base_idx < len(new_rh):
1827
1938
        to_file.write('Added Revisions:\n')
1830
1941
                 None,
1831
1942
                 verbose=False,
1832
1943
                 direction='forward',
1833
 
                 start_revision=base_idx+1,
 
1944
                 start_revision=base_idx + 1,
1834
1945
                 end_revision=len(new_rh),
1835
1946
                 search=None)
1836
1947
 
1848
1959
    old_revisions = set()
1849
1960
    new_history = []
1850
1961
    new_revisions = set()
1851
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1852
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
 
1962
    graph = repository.get_graph()
 
1963
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1964
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
1853
1965
    stop_revision = None
1854
1966
    do_old = True
1855
1967
    do_new = True
1856
1968
    while do_new or do_old:
1857
1969
        if do_new:
1858
1970
            try:
1859
 
                new_revision = new_iter.next()
 
1971
                new_revision = next(new_iter)
1860
1972
            except StopIteration:
1861
1973
                do_new = False
1862
1974
            else:
1867
1979
                    break
1868
1980
        if do_old:
1869
1981
            try:
1870
 
                old_revision = old_iter.next()
 
1982
                old_revision = next(old_iter)
1871
1983
            except StopIteration:
1872
1984
                do_old = False
1873
1985
            else:
1903
2015
    log_format = log_formatter_registry.get_default(branch)
1904
2016
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
1905
2017
    if old_history != []:
1906
 
        output.write('*'*60)
 
2018
        output.write('*' * 60)
1907
2019
        output.write('\nRemoved Revisions:\n')
1908
2020
        show_flat_log(branch.repository, old_history, old_revno, lf)
1909
 
        output.write('*'*60)
 
2021
        output.write('*' * 60)
1910
2022
        output.write('\n\n')
1911
2023
    if new_history != []:
1912
2024
        output.write('Added Revisions:\n')
1913
2025
        start_revno = new_revno - len(new_history) + 1
1914
2026
        show_log(branch, lf, None, verbose=False, direction='forward',
1915
 
                 start_revision=start_revno,)
 
2027
                 start_revision=start_revno)
1916
2028
 
1917
2029
 
1918
2030
def show_flat_log(repository, history, last_revno, lf):
1930
2042
        lf.log_revision(lr)
1931
2043
 
1932
2044
 
1933
 
def _get_info_for_log_files(revisionspec_list, file_list):
 
2045
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
1934
2046
    """Find file-ids and kinds given a list of files and a revision range.
1935
2047
 
1936
2048
    We search for files at the end of the range. If not found there,
1940
2052
    :param file_list: the list of paths given on the command line;
1941
2053
      the first of these can be a branch location or a file path,
1942
2054
      the remainder must be file paths
 
2055
    :param add_cleanup: When the branch returned is read locked,
 
2056
      an unlock call will be queued to the cleanup.
1943
2057
    :return: (branch, info_list, start_rev_info, end_rev_info) where
1944
2058
      info_list is a list of (relative_path, file_id, kind) tuples where
1945
2059
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1946
2060
      branch will be read-locked.
1947
2061
    """
1948
 
    from builtins import _get_revision_range, safe_relpath_files
1949
 
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
1950
 
    b.lock_read()
 
2062
    from breezy.builtins import _get_revision_range
 
2063
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2064
        file_list[0])
 
2065
    add_cleanup(b.lock_read().unlock)
1951
2066
    # XXX: It's damn messy converting a list of paths to relative paths when
1952
2067
    # those paths might be deleted ones, they might be on a case-insensitive
1953
2068
    # filesystem and/or they might be in silly locations (like another branch).
1957
2072
    # case of running log in a nested directory, assuming paths beyond the
1958
2073
    # first one haven't been deleted ...
1959
2074
    if tree:
1960
 
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
2075
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
1961
2076
    else:
1962
2077
        relpaths = [path] + file_list[1:]
1963
2078
    info_list = []
1964
2079
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
1965
 
        "log")
 
2080
                                                       "log")
1966
2081
    if relpaths in ([], [u'']):
1967
2082
        return b, [], start_rev_info, end_rev_info
1968
2083
    if start_rev_info is None and end_rev_info is None:
1971
2086
        tree1 = None
1972
2087
        for fp in relpaths:
1973
2088
            file_id = tree.path2id(fp)
1974
 
            kind = _get_kind_for_file_id(tree, file_id)
 
2089
            kind = _get_kind_for_file_id(tree, fp, file_id)
1975
2090
            if file_id is None:
1976
2091
                # go back to when time began
1977
2092
                if tree1 is None:
1985
2100
                        tree1 = b.repository.revision_tree(rev1)
1986
2101
                if tree1:
1987
2102
                    file_id = tree1.path2id(fp)
1988
 
                    kind = _get_kind_for_file_id(tree1, file_id)
 
2103
                    kind = _get_kind_for_file_id(tree1, fp, file_id)
1989
2104
            info_list.append((fp, file_id, kind))
1990
2105
 
1991
2106
    elif start_rev_info == end_rev_info:
1993
2108
        tree = b.repository.revision_tree(end_rev_info.rev_id)
1994
2109
        for fp in relpaths:
1995
2110
            file_id = tree.path2id(fp)
1996
 
            kind = _get_kind_for_file_id(tree, file_id)
 
2111
            kind = _get_kind_for_file_id(tree, fp, file_id)
1997
2112
            info_list.append((fp, file_id, kind))
1998
2113
 
1999
2114
    else:
2007
2122
        tree1 = None
2008
2123
        for fp in relpaths:
2009
2124
            file_id = tree.path2id(fp)
2010
 
            kind = _get_kind_for_file_id(tree, file_id)
 
2125
            kind = _get_kind_for_file_id(tree, fp, file_id)
2011
2126
            if file_id is None:
2012
2127
                if tree1 is None:
2013
2128
                    rev_id = start_rev_info.rev_id
2017
2132
                    else:
2018
2133
                        tree1 = b.repository.revision_tree(rev_id)
2019
2134
                file_id = tree1.path2id(fp)
2020
 
                kind = _get_kind_for_file_id(tree1, file_id)
 
2135
                kind = _get_kind_for_file_id(tree1, fp, file_id)
2021
2136
            info_list.append((fp, file_id, kind))
2022
2137
    return b, info_list, start_rev_info, end_rev_info
2023
2138
 
2024
2139
 
2025
 
def _get_kind_for_file_id(tree, file_id):
 
2140
def _get_kind_for_file_id(tree, path, file_id):
2026
2141
    """Return the kind of a file-id or None if it doesn't exist."""
2027
2142
    if file_id is not None:
2028
 
        return tree.kind(file_id)
 
2143
        return tree.kind(path, file_id)
2029
2144
    else:
2030
2145
        return None
2031
2146
 
2033
2148
properties_handler_registry = registry.Registry()
2034
2149
 
2035
2150
# Use the properties handlers to print out bug information if available
 
2151
 
 
2152
 
2036
2153
def _bugs_properties_handler(revision):
2037
 
    if revision.properties.has_key('bugs'):
 
2154
    if 'bugs' in revision.properties:
2038
2155
        bug_lines = revision.properties['bugs'].split('\n')
2039
2156
        bug_rows = [line.split(' ', 1) for line in bug_lines]
2040
2157
        fixed_bug_urls = [row[0] for row in bug_rows if
2041
2158
                          len(row) > 1 and row[1] == 'fixed']
2042
2159
 
2043
2160
        if fixed_bug_urls:
2044
 
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2161
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):
 
2162
                    ' '.join(fixed_bug_urls)}
2045
2163
    return {}
2046
2164
 
 
2165
 
2047
2166
properties_handler_registry.register('bugs_properties_handler',
2048
2167
                                     _bugs_properties_handler)
2049
2168