/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: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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