/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-16 18:15:40 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-20181116181540-7y2wbhqzjk067mqy
Fix repo acquisition.

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