/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 bzrlib/log.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-02-11 04:02:41 UTC
  • mfrom: (5017.2.2 tariff)
  • Revision ID: pqm@pqm.ubuntu.com-20100211040241-w6n021dz0uus341n
(mbp) add import-tariff tests

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2010 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
 
17
19
"""Code to show logs of changes.
18
20
 
19
21
Various flavors of log can be produced:
47
49
all the changes since the previous revision that touched hello.c.
48
50
"""
49
51
 
50
 
from __future__ import absolute_import
51
 
 
52
52
import codecs
53
 
import itertools
 
53
from cStringIO import StringIO
 
54
from itertools import (
 
55
    chain,
 
56
    izip,
 
57
    )
54
58
import re
55
59
import sys
56
60
from warnings import (
57
61
    warn,
58
62
    )
59
63
 
60
 
from .lazy_import import lazy_import
 
64
from bzrlib.lazy_import import lazy_import
61
65
lazy_import(globals(), """
62
66
 
63
 
from breezy import (
 
67
from bzrlib import (
 
68
    bzrdir,
64
69
    config,
65
 
    controldir,
66
70
    diff,
 
71
    errors,
67
72
    foreign,
68
 
    lazy_regex,
 
73
    repository as _mod_repository,
69
74
    revision as _mod_revision,
 
75
    revisionspec,
 
76
    trace,
 
77
    tsort,
70
78
    )
71
 
from breezy.i18n import gettext, ngettext
72
79
""")
73
80
 
74
 
from . import (
75
 
    errors,
 
81
from bzrlib import (
76
82
    registry,
77
 
    revisionspec,
78
 
    trace,
79
83
    )
80
 
from .osutils import (
 
84
from bzrlib.osutils import (
81
85
    format_date,
82
86
    format_date_with_offset_in_original_timezone,
83
 
    get_diff_header_encoding,
84
87
    get_terminal_encoding,
 
88
    re_compile_checked,
85
89
    terminal_width,
86
90
    )
87
 
from .sixish import (
88
 
    BytesIO,
89
 
    range,
90
 
    zip,
 
91
from bzrlib.symbol_versioning import (
 
92
    deprecated_function,
 
93
    deprecated_in,
91
94
    )
92
 
from .tree import InterTree
93
 
 
94
 
 
95
 
def find_touching_revisions(repository, last_revision, last_tree, last_path):
 
95
 
 
96
 
 
97
def find_touching_revisions(branch, file_id):
96
98
    """Yield a description of revisions which affect the file_id.
97
99
 
98
100
    Each returned element is (revno, revision_id, description)
103
105
    TODO: Perhaps some way to limit this to only particular revisions,
104
106
    or to traverse a non-mainline set of revisions?
105
107
    """
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)
 
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_revision_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
114
118
 
115
119
        # now we know how it was last time, and how it is in this revision.
116
120
        # are those two states effectively the same or not?
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
 
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
122
130
        elif this_path != last_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
 
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
129
135
 
130
 
        last_verifier = this_verifier
 
136
        last_ie = this_ie
131
137
        last_path = this_path
132
 
        last_tree = this_tree
133
 
        if last_path is None:
134
 
            return
135
 
        revno -= 1
 
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
136
148
 
137
149
 
138
150
def show_log(branch,
139
151
             lf,
 
152
             specific_fileid=None,
140
153
             verbose=False,
141
154
             direction='reverse',
142
155
             start_revision=None,
143
156
             end_revision=None,
144
157
             search=None,
145
158
             limit=None,
146
 
             show_diff=False,
147
 
             match=None):
 
159
             show_diff=False):
148
160
    """Write out human-readable log of commits to this branch.
149
161
 
150
162
    This function is being retained for backwards compatibility but
154
166
 
155
167
    :param lf: The LogFormatter object showing the output.
156
168
 
 
169
    :param specific_fileid: If not None, list only the commits affecting the
 
170
        specified file, rather than all commits.
 
171
 
157
172
    :param verbose: If True show added/changed/deleted/renamed files.
158
173
 
159
174
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
170
185
        if None or 0.
171
186
 
172
187
    :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.
176
188
    """
 
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
177
194
    if verbose:
178
 
        delta_type = 'full'
 
195
        if file_ids:
 
196
            delta_type = 'partial'
 
197
        else:
 
198
            delta_type = 'full'
179
199
    else:
180
200
        delta_type = None
181
201
    if show_diff:
182
 
        diff_type = 'full'
 
202
        if file_ids:
 
203
            diff_type = 'partial'
 
204
        else:
 
205
            diff_type = 'full'
183
206
    else:
184
207
        diff_type = None
185
208
 
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
 
 
201
209
    # Build the request and execute it
202
 
    rqst = make_log_request_dict(
203
 
        direction=direction,
 
210
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
204
211
        start_revision=start_revision, end_revision=end_revision,
205
212
        limit=limit, message_search=search,
206
213
        delta_type=delta_type, diff_type=diff_type)
207
214
    Logger(branch, rqst).show(lf)
208
215
 
209
216
 
210
 
# Note: This needs to be kept in sync with the defaults in
 
217
# Note: This needs to be kept this in sync with the defaults in
211
218
# make_log_request_dict() below
212
219
_DEFAULT_REQUEST_PARAMS = {
213
220
    'direction': 'reverse',
214
 
    'levels': None,
 
221
    'levels': 1,
215
222
    'generate_tags': True,
216
 
    'exclude_common_ancestry': False,
217
223
    '_match_using_deltas': True,
218
224
    }
219
225
 
220
226
 
221
227
def make_log_request_dict(direction='reverse', specific_fileids=None,
222
 
                          start_revision=None, end_revision=None, limit=None,
223
 
                          message_search=None, levels=None, generate_tags=True,
224
 
                          delta_type=None,
225
 
                          diff_type=None, _match_using_deltas=True,
226
 
                          exclude_common_ancestry=False, match=None,
227
 
                          signature=False, omit_merges=False,
228
 
                          ):
 
228
    start_revision=None, end_revision=None, limit=None,
 
229
    message_search=None, levels=1, generate_tags=True, delta_type=None,
 
230
    diff_type=None, _match_using_deltas=True):
229
231
    """Convenience function for making a logging request dictionary.
230
232
 
231
233
    Using this function may make code slightly safer by ensuring
251
253
      matching commit messages
252
254
 
253
255
    :param levels: the number of levels of revisions to
254
 
      generate; 1 for just the mainline; 0 for all levels, or None for
255
 
      a sensible default.
 
256
      generate; 1 for just the mainline; 0 for all levels.
256
257
 
257
258
    :param generate_tags: If True, include tags for matched revisions.
258
 
`
 
259
 
259
260
    :param delta_type: Either 'full', 'partial' or None.
260
261
      'full' means generate the complete delta - adds/deletes/modifies/etc;
261
262
      'partial' means filter the delta using specific_fileids;
268
269
 
269
270
    :param _match_using_deltas: a private parameter controlling the
270
271
      algorithm used for matching specific_fileids. This parameter
271
 
      may be removed in the future so breezy client code should NOT
 
272
      may be removed in the future so bzrlib client code should NOT
272
273
      use it.
273
 
 
274
 
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
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
 
 
286
274
    """
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]}
296
275
    return {
297
276
        'direction': direction,
298
277
        'specific_fileids': specific_fileids,
299
278
        'start_revision': start_revision,
300
279
        'end_revision': end_revision,
301
280
        'limit': limit,
 
281
        'message_search': message_search,
302
282
        'levels': levels,
303
283
        'generate_tags': generate_tags,
304
284
        'delta_type': delta_type,
305
285
        'diff_type': diff_type,
306
 
        'exclude_common_ancestry': exclude_common_ancestry,
307
 
        'signature': signature,
308
 
        'match': match,
309
 
        'omit_merges': omit_merges,
310
286
        # Add 'private' attributes for features that may be deprecated
311
287
        '_match_using_deltas': _match_using_deltas,
312
288
    }
314
290
 
315
291
def _apply_log_request_defaults(rqst):
316
292
    """Apply default values to a request dictionary."""
317
 
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
293
    result = _DEFAULT_REQUEST_PARAMS
318
294
    if rqst:
319
295
        result.update(rqst)
320
296
    return result
321
297
 
322
298
 
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
 
 
344
299
class LogGenerator(object):
345
300
    """A generator of log revisions."""
346
301
 
373
328
        if not isinstance(lf, LogFormatter):
374
329
            warn("not a LogFormatter instance: %r" % lf)
375
330
 
376
 
        with self.branch.lock_read():
 
331
        self.branch.lock_read()
 
332
        try:
377
333
            if getattr(lf, 'begin_log', None):
378
334
                lf.begin_log()
379
335
            self._show_body(lf)
380
336
            if getattr(lf, 'end_log', None):
381
337
                lf.end_log()
 
338
        finally:
 
339
            self.branch.unlock()
382
340
 
383
341
    def _show_body(self, lf):
384
342
        """Show the main log output.
388
346
        # Tweak the LogRequest based on what the LogFormatter can handle.
389
347
        # (There's no point generating stuff if the formatter can't display it.)
390
348
        rqst = self.rqst
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
 
 
 
349
        rqst['levels'] = lf.get_levels()
395
350
        if not getattr(lf, 'supports_tags', False):
396
351
            rqst['generate_tags'] = False
397
352
        if not getattr(lf, 'supports_delta', False):
398
353
            rqst['delta_type'] = None
399
354
        if not getattr(lf, 'supports_diff', False):
400
355
            rqst['diff_type'] = None
401
 
        if not getattr(lf, 'supports_signatures', False):
402
 
            rqst['signature'] = False
403
356
 
404
357
        # Find and print the interesting revisions
405
358
        generator = self._generator_factory(self.branch, rqst)
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.'))
 
359
        for lr in generator.iter_log_revisions():
 
360
            lf.log_revision(lr)
412
361
        lf.show_advice()
413
362
 
414
363
    def _generator_factory(self, branch, rqst):
415
364
        """Make the LogGenerator object to use.
416
 
 
 
365
        
417
366
        Subclasses may wish to override this.
418
367
        """
419
368
        return _DefaultLogGenerator(branch, rqst)
443
392
        levels = rqst.get('levels')
444
393
        limit = rqst.get('limit')
445
394
        diff_type = rqst.get('diff_type')
446
 
        show_signature = rqst.get('signature')
447
 
        omit_merges = rqst.get('omit_merges')
448
395
        log_count = 0
449
396
        revision_iterator = self._create_log_revision_iterator()
450
397
        for revs in revision_iterator:
451
398
            for (rev_id, revno, merge_depth), rev, delta in revs:
452
399
                # 0 levels means show everything; merge_depth counts from 0
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)
 
400
                if levels != 0 and merge_depth >= levels:
 
401
                    continue
460
402
                if diff_type is None:
461
403
                    diff = None
462
404
                else:
463
405
                    diff = self._format_diff(rev, rev_id, diff_type)
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)
 
406
                yield LogRevision(rev, revno, merge_depth, delta,
 
407
                    self.rev_tag_dict.get(rev_id), diff)
471
408
                if limit:
472
409
                    log_count += 1
473
410
                    if log_count >= limit:
486
423
            specific_files = [tree_2.id2path(id) for id in file_ids]
487
424
        else:
488
425
            specific_files = None
489
 
        s = BytesIO()
490
 
        path_encoding = get_diff_header_encoding()
 
426
        s = StringIO()
491
427
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
492
 
                             new_label='', path_encoding=path_encoding)
 
428
            new_label='')
493
429
        return s.getvalue()
494
430
 
495
431
    def _create_log_revision_iterator(self):
509
445
            # not a directory
510
446
            file_count = len(self.rqst.get('specific_fileids'))
511
447
            if file_count != 1:
512
 
                raise errors.BzrError(
513
 
                    "illegal LogRequest: must match-using-deltas "
 
448
                raise BzrError("illegal LogRequest: must match-using-deltas "
514
449
                    "when logging %d files" % file_count)
515
450
            return self._log_revision_iterator_using_per_file_graph()
516
451
 
519
454
        rqst = self.rqst
520
455
        generate_merge_revisions = rqst.get('levels') != 1
521
456
        delayed_graph_generation = not rqst.get('specific_fileids') and (
522
 
            rqst.get('limit') or self.start_rev_id or self.end_rev_id)
523
 
        view_revisions = _calc_view_revisions(
524
 
            self.branch, self.start_rev_id, self.end_rev_id,
525
 
            rqst.get('direction'),
526
 
            generate_merge_revisions=generate_merge_revisions,
527
 
            delayed_graph_generation=delayed_graph_generation,
528
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
457
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
458
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
459
            self.end_rev_id, rqst.get('direction'), generate_merge_revisions,
 
460
            delayed_graph_generation=delayed_graph_generation)
529
461
 
530
462
        # Apply the other filters
531
463
        return make_log_rev_iterator(self.branch, view_revisions,
532
 
                                     rqst.get('delta_type'), rqst.get('match'),
533
 
                                     file_ids=rqst.get('specific_fileids'),
534
 
                                     direction=rqst.get('direction'))
 
464
            rqst.get('delta_type'), rqst.get('message_search'),
 
465
            file_ids=rqst.get('specific_fileids'),
 
466
            direction=rqst.get('direction'))
535
467
 
536
468
    def _log_revision_iterator_using_per_file_graph(self):
537
469
        # Get the base revisions, filtering by the revision range.
538
470
        # Note that we always generate the merge revisions because
539
471
        # filter_revisions_touching_file_id() requires them ...
540
472
        rqst = self.rqst
541
 
        view_revisions = _calc_view_revisions(
542
 
            self.branch, self.start_rev_id, self.end_rev_id,
543
 
            rqst.get('direction'), generate_merge_revisions=True,
544
 
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
473
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
474
            self.end_rev_id, rqst.get('direction'), True)
545
475
        if not isinstance(view_revisions, list):
546
476
            view_revisions = list(view_revisions)
547
477
        view_revisions = _filter_revisions_touching_file_id(self.branch,
548
 
                                                            rqst.get('specific_fileids')[
549
 
                                                                0], view_revisions,
550
 
                                                            include_merges=rqst.get('levels') != 1)
 
478
            rqst.get('specific_fileids')[0], view_revisions,
 
479
            include_merges=rqst.get('levels') != 1)
551
480
        return make_log_rev_iterator(self.branch, view_revisions,
552
 
                                     rqst.get('delta_type'), rqst.get('match'))
 
481
            rqst.get('delta_type'), rqst.get('message_search'))
553
482
 
554
483
 
555
484
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
556
 
                         generate_merge_revisions,
557
 
                         delayed_graph_generation=False,
558
 
                         exclude_common_ancestry=False,
559
 
                         ):
 
485
    generate_merge_revisions, delayed_graph_generation=False):
560
486
    """Calculate the revisions to view.
561
487
 
562
488
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
563
489
             a list of the same tuples.
564
490
    """
565
 
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
566
 
        raise errors.BzrCommandError(gettext(
567
 
            '--exclude-common-ancestry requires two different revisions'))
568
 
    if direction not in ('reverse', 'forward'):
569
 
        raise ValueError(gettext('invalid direction %r') % direction)
570
 
    br_rev_id = branch.last_revision()
571
 
    if br_rev_id == _mod_revision.NULL_REVISION:
 
491
    br_revno, br_rev_id = branch.last_revision_info()
 
492
    if br_revno == 0:
572
493
        return []
573
494
 
574
 
    if (end_rev_id and start_rev_id == end_rev_id
575
 
        and (not generate_merge_revisions
576
 
             or not _has_merges(branch, end_rev_id))):
577
 
        # If a single revision is requested, check we can handle it
578
 
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
579
 
                                      branch.revno())
 
495
    # If a single revision is requested, check we can handle it
 
496
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
 
497
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
 
498
    if generate_single_revision:
 
499
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno)
 
500
 
 
501
    # If we only want to see linear revisions, we can iterate ...
580
502
    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)))
604
 
    return iter_revs
 
503
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
504
            direction)
 
505
    else:
 
506
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
507
            direction, delayed_graph_generation)
605
508
 
606
509
 
607
510
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
609
512
        # It's the tip
610
513
        return [(br_rev_id, br_revno, 0)]
611
514
    else:
612
 
        revno_str = _compute_revno_str(branch, rev_id)
 
515
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
516
        revno_str = '.'.join(str(n) for n in revno)
613
517
        return [(rev_id, revno_str, 0)]
614
518
 
615
519
 
 
520
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
 
521
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
 
522
    # If a start limit was given and it's not obviously an
 
523
    # ancestor of the end limit, check it before outputting anything
 
524
    if direction == 'forward' or (start_rev_id
 
525
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
 
526
        try:
 
527
            result = list(result)
 
528
        except _StartNotLinearAncestor:
 
529
            raise errors.BzrCommandError('Start revision not found in'
 
530
                ' left-hand history of end revision.')
 
531
    if direction == 'forward':
 
532
        result = reversed(result)
 
533
    return result
 
534
 
 
535
 
616
536
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
617
 
                            delayed_graph_generation,
618
 
                            exclude_common_ancestry=False):
 
537
                            delayed_graph_generation):
619
538
    # On large trees, generating the merge graph can take 30-60 seconds
620
539
    # so we delay doing it until a merge is detected, incrementally
621
540
    # returning initial (non-merge) revisions while we can.
626
545
    initial_revisions = []
627
546
    if delayed_graph_generation:
628
547
        try:
629
 
            for rev_id, revno, depth in _linear_view_revisions(
630
 
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
548
            for rev_id, revno, depth in  _linear_view_revisions(
 
549
                branch, start_rev_id, end_rev_id):
631
550
                if _has_merges(branch, rev_id):
632
551
                    # The end_rev_id can be nested down somewhere. We need an
633
552
                    # explicit ancestry check. There is an ambiguity here as we
634
553
                    # may not raise _StartNotLinearAncestor for a revision that
635
554
                    # is an ancestor but not a *linear* one. But since we have
636
555
                    # loaded the graph to do the check (or calculate a dotted
637
 
                    # revno), we may as well accept to show the log...  We need
638
 
                    # the check only if start_rev_id is not None as all
639
 
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
640
 
                    # -- vila 20100319
 
556
                    # revno), we may as well accept to show the log... 
 
557
                    # -- vila 100201
641
558
                    graph = branch.repository.get_graph()
642
 
                    if (start_rev_id is not None
643
 
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
559
                    if not graph.is_ancestor(start_rev_id, end_rev_id):
644
560
                        raise _StartNotLinearAncestor()
645
 
                    # Since we collected the revisions so far, we need to
646
 
                    # adjust end_rev_id.
647
561
                    end_rev_id = rev_id
648
562
                    break
649
563
                else:
650
564
                    initial_revisions.append((rev_id, revno, depth))
651
565
            else:
652
566
                # No merged revisions found
653
 
                return initial_revisions
 
567
                if direction == 'reverse':
 
568
                    return initial_revisions
 
569
                elif direction == 'forward':
 
570
                    return reversed(initial_revisions)
 
571
                else:
 
572
                    raise ValueError('invalid direction %r' % direction)
654
573
        except _StartNotLinearAncestor:
655
574
            # A merge was never detected so the lower revision limit can't
656
575
            # be nested down somewhere
657
 
            raise errors.BzrCommandError(gettext('Start revision not found in'
658
 
                                                 ' history of end revision.'))
659
 
 
660
 
    # We exit the loop above because we encounter a revision with merges, from
661
 
    # this revision, we need to switch to _graph_view_revisions.
 
576
            raise errors.BzrCommandError('Start revision not found in'
 
577
                ' history of end revision.')
662
578
 
663
579
    # A log including nested merges is required. If the direction is reverse,
664
580
    # we rebase the initial merge depths so that the development line is
665
581
    # shown naturally, i.e. just like it is for linear logging. We can easily
666
582
    # make forward the exact opposite display, but showing the merge revisions
667
583
    # indented at the end seems slightly nicer in that case.
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))
673
 
    return view_revisions
 
584
    view_revisions = chain(iter(initial_revisions),
 
585
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
586
        rebase_initial_depths=direction == 'reverse'))
 
587
    if direction == 'reverse':
 
588
        return view_revisions
 
589
    elif direction == 'forward':
 
590
        # Forward means oldest first, adjusting for depth.
 
591
        view_revisions = reverse_by_depth(list(view_revisions))
 
592
        return _rebase_merge_depth(view_revisions)
 
593
    else:
 
594
        raise ValueError('invalid direction %r' % direction)
674
595
 
675
596
 
676
597
def _has_merges(branch, rev_id):
679
600
    return len(parents) > 1
680
601
 
681
602
 
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
 
 
697
603
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
698
604
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
699
605
    if start_rev_id and 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
 
606
        start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
607
        end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
706
608
        if len(start_dotted) == 1 and len(end_dotted) == 1:
707
609
            # both on mainline
708
610
            return start_dotted[0] <= end_dotted[0]
709
611
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
710
 
              start_dotted[0:1] == end_dotted[0:1]):
 
612
            start_dotted[0:1] == end_dotted[0:1]):
711
613
            # both on same development line
712
614
            return start_dotted[2] <= end_dotted[2]
713
615
        else:
718
620
    return True
719
621
 
720
622
 
721
 
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
722
 
                           exclude_common_ancestry=False):
 
623
def _linear_view_revisions(branch, start_rev_id, end_rev_id):
723
624
    """Calculate a sequence of revisions to view, newest to oldest.
724
625
 
725
626
    :param start_rev_id: the lower revision-id
726
627
    :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.
729
628
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
730
 
        dotted_revno will be None for ghosts
731
629
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
732
 
        is not found walking the left-hand history
 
630
      is not found walking the left-hand history
733
631
    """
 
632
    br_revno, br_rev_id = branch.last_revision_info()
734
633
    repo = branch.repository
735
 
    graph = repo.get_graph()
736
634
    if start_rev_id is None and end_rev_id is None:
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
 
635
        cur_revno = br_revno
 
636
        for revision_id in repo.iter_reverse_revision_history(br_rev_id):
 
637
            yield revision_id, str(cur_revno), 0
 
638
            cur_revno -= 1
765
639
    else:
766
 
        br_rev_id = branch.last_revision()
767
640
        if end_rev_id is None:
768
641
            end_rev_id = br_rev_id
769
642
        found_start = start_rev_id is None
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
 
643
        for revision_id in repo.iter_reverse_revision_history(end_rev_id):
 
644
            revno = branch.revision_id_to_dotted_revno(revision_id)
 
645
            revno_str = '.'.join(str(n) for n in revno)
 
646
            if not found_start and revision_id == start_rev_id:
 
647
                yield revision_id, revno_str, 0
 
648
                found_start = True
780
649
                break
781
650
            else:
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()
 
651
                yield revision_id, revno_str, 0
 
652
        else:
 
653
            if not found_start:
 
654
                raise _StartNotLinearAncestor()
792
655
 
793
656
 
794
657
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
795
 
                          rebase_initial_depths=True,
796
 
                          exclude_common_ancestry=False):
 
658
    rebase_initial_depths=True):
797
659
    """Calculate revisions to view including merges, newest to oldest.
798
660
 
799
661
    :param branch: the branch
803
665
      revision is found?
804
666
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
805
667
    """
806
 
    if exclude_common_ancestry:
807
 
        stop_rule = 'with-merges-without-common-ancestry'
808
 
    else:
809
 
        stop_rule = 'with-merges'
810
668
    view_revisions = branch.iter_merge_sorted_revisions(
811
669
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
812
 
        stop_rule=stop_rule)
 
670
        stop_rule="with-merges")
813
671
    if not rebase_initial_depths:
814
672
        for (rev_id, merge_depth, revno, end_of_merge
815
673
             ) in view_revisions:
835
693
            yield rev_id, '.'.join(map(str, revno)), merge_depth
836
694
 
837
695
 
 
696
@deprecated_function(deprecated_in((2, 2, 0)))
 
697
def calculate_view_revisions(branch, start_revision, end_revision, direction,
 
698
        specific_fileid, generate_merge_revisions):
 
699
    """Calculate the revisions to view.
 
700
 
 
701
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
 
702
             a list of the same tuples.
 
703
    """
 
704
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
 
705
        end_revision)
 
706
    view_revisions = list(_calc_view_revisions(branch, start_rev_id, end_rev_id,
 
707
        direction, generate_merge_revisions or specific_fileid))
 
708
    if specific_fileid:
 
709
        view_revisions = _filter_revisions_touching_file_id(branch,
 
710
            specific_fileid, view_revisions,
 
711
            include_merges=generate_merge_revisions)
 
712
    return _rebase_merge_depth(view_revisions)
 
713
 
 
714
 
838
715
def _rebase_merge_depth(view_revisions):
839
716
    """Adjust depths upwards so the top level is 0."""
840
717
    # If either the first or last revision have a merge_depth of 0, we're done
841
718
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
842
 
        min_depth = min([d for r, n, d in view_revisions])
 
719
        min_depth = min([d for r,n,d in view_revisions])
843
720
        if min_depth != 0:
844
 
            view_revisions = [(r, n, d - min_depth)
845
 
                              for r, n, d in view_revisions]
 
721
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
846
722
    return view_revisions
847
723
 
848
724
 
849
725
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
850
 
                          file_ids=None, direction='reverse'):
 
726
        file_ids=None, direction='reverse'):
851
727
    """Create a revision iterator for log.
852
728
 
853
729
    :param branch: The branch being logged.
863
739
    """
864
740
    # Convert view_revisions into (view, None, None) groups to fit with
865
741
    # the standard interface here.
866
 
    if isinstance(view_revisions, list):
 
742
    if type(view_revisions) == list:
867
743
        # A single batch conversion is faster than many incremental ones.
868
744
        # As we have all the data, do a batch conversion.
869
745
        nones = [None] * len(view_revisions)
870
 
        log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
 
746
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
871
747
    else:
872
748
        def _convert():
873
749
            for view in view_revisions:
877
753
        # It would be nicer if log adapters were first class objects
878
754
        # with custom parameters. This will do for now. IGC 20090127
879
755
        if adapter == _make_delta_filter:
880
 
            log_rev_iterator = adapter(
881
 
                branch, generate_delta, search, log_rev_iterator, file_ids,
882
 
                direction)
 
756
            log_rev_iterator = adapter(branch, generate_delta,
 
757
                search, log_rev_iterator, file_ids, direction)
883
758
        else:
884
 
            log_rev_iterator = adapter(
885
 
                branch, generate_delta, search, log_rev_iterator)
 
759
            log_rev_iterator = adapter(branch, generate_delta,
 
760
                search, log_rev_iterator)
886
761
    return log_rev_iterator
887
762
 
888
763
 
889
 
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
 
764
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
890
765
    """Create a filtered iterator of log_rev_iterator matching on a regex.
891
766
 
892
767
    :param branch: The branch being logged.
893
768
    :param generate_delta: Whether to generate a delta for each revision.
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.
 
769
    :param search: A user text search string.
898
770
    :param log_rev_iterator: An input iterator containing all revisions that
899
771
        could be displayed, in lists.
900
772
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
901
773
        delta).
902
774
    """
903
 
    if not match:
 
775
    if search is None:
904
776
        return 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):
 
777
    searchRE = re_compile_checked(search, re.IGNORECASE,
 
778
            'log message filter')
 
779
    return _filter_message_re(searchRE, log_rev_iterator)
 
780
 
 
781
 
 
782
def _filter_message_re(searchRE, log_rev_iterator):
912
783
    for revs in log_rev_iterator:
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)
 
784
        new_revs = []
 
785
        for (rev_id, revno, merge_depth), rev, delta in revs:
 
786
            if searchRE.search(rev.message):
 
787
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
 
788
        yield new_revs
935
789
 
936
790
 
937
791
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
938
 
                       fileids=None, direction='reverse'):
 
792
    fileids=None, direction='reverse'):
939
793
    """Add revision deltas to a log iterator if needed.
940
794
 
941
795
    :param branch: The branch being logged.
953
807
    if not generate_delta and not fileids:
954
808
        return log_rev_iterator
955
809
    return _generate_deltas(branch.repository, log_rev_iterator,
956
 
                            generate_delta, fileids, direction)
 
810
        generate_delta, fileids, direction)
957
811
 
958
812
 
959
813
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
960
 
                     direction):
 
814
    direction):
961
815
    """Create deltas for each batch of revisions in log_rev_iterator.
962
816
 
963
817
    If we're only generating deltas for the sake of filtering against
983
837
        new_revs = []
984
838
        if delta_type == 'full' and not check_fileids:
985
839
            deltas = repository.get_deltas_for_revisions(revisions)
986
 
            for rev, delta in zip(revs, deltas):
 
840
            for rev, delta in izip(revs, deltas):
987
841
                new_revs.append((rev[0], rev[1], delta))
988
842
        else:
989
843
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
990
 
            for rev, delta in zip(revs, deltas):
 
844
            for rev, delta in izip(revs, deltas):
991
845
                if check_fileids:
992
846
                    if delta is None or not delta.has_changed():
993
847
                        continue
1011
865
 
1012
866
def _update_fileids(delta, fileids, stop_on):
1013
867
    """Update the set of file-ids to search based on file lifecycle events.
1014
 
 
 
868
    
1015
869
    :param fileids: a set of fileids to update
1016
870
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
1017
871
      fileids set once their add or remove entry is detected respectively
1018
872
    """
1019
873
    if stop_on == 'add':
1020
 
        for item in delta.added + delta.copied:
1021
 
            if item.file_id in fileids:
1022
 
                fileids.remove(item.file_id)
 
874
        for item in delta.added:
 
875
            if item[1] in fileids:
 
876
                fileids.remove(item[1])
1023
877
    elif stop_on == 'delete':
1024
878
        for item in delta.removed:
1025
 
            if item.file_id in fileids:
1026
 
                fileids.remove(item.file_id)
 
879
            if item[1] in fileids:
 
880
                fileids.remove(item[1])
1027
881
 
1028
882
 
1029
883
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1041
895
    for revs in log_rev_iterator:
1042
896
        # r = revision_id, n = revno, d = merge depth
1043
897
        revision_ids = [view[0] for view, _, _ in revs]
1044
 
        revisions = dict(repository.iter_revisions(revision_ids))
1045
 
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
 
898
        revisions = repository.get_revisions(revision_ids)
 
899
        revs = [(rev[0], revision, rev[2]) for rev, revision in
 
900
            izip(revs, revisions)]
 
901
        yield revs
1046
902
 
1047
903
 
1048
904
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
1056
912
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
1057
913
        delta).
1058
914
    """
 
915
    repository = branch.repository
1059
916
    num = 9
1060
917
    for batch in log_rev_iterator:
1061
918
        batch = iter(batch)
1073
930
    :param  branch: The branch containing the revisions.
1074
931
 
1075
932
    :param  start_revision: The first revision to be logged.
 
933
            For backwards compatibility this may be a mainline integer revno,
1076
934
            but for merge revision support a RevisionInfo is expected.
1077
935
 
1078
936
    :param  end_revision: The last revision to be logged.
1081
939
 
1082
940
    :return: (start_rev_id, end_rev_id) tuple.
1083
941
    """
 
942
    branch_revno, branch_rev_id = branch.last_revision_info()
1084
943
    start_rev_id = 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:
 
944
    if start_revision is None:
1092
945
        start_revno = 1
 
946
    else:
 
947
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
948
            start_rev_id = start_revision.rev_id
 
949
            start_revno = start_revision.revno or 1
 
950
        else:
 
951
            branch.check_real_revno(start_revision)
 
952
            start_revno = start_revision
 
953
            start_rev_id = branch.get_rev_id(start_revno)
1093
954
 
1094
955
    end_rev_id = None
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
 
956
    if end_revision is None:
 
957
        end_revno = branch_revno
 
958
    else:
 
959
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
960
            end_rev_id = end_revision.rev_id
 
961
            end_revno = end_revision.revno or branch_revno
 
962
        else:
 
963
            branch.check_real_revno(end_revision)
 
964
            end_revno = end_revision
 
965
            end_rev_id = branch.get_rev_id(end_revno)
1101
966
 
1102
 
    if branch.last_revision() != _mod_revision.NULL_REVISION:
 
967
    if branch_revno != 0:
1103
968
        if (start_rev_id == _mod_revision.NULL_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."))
 
969
            or end_rev_id == _mod_revision.NULL_REVISION):
 
970
            raise errors.BzrCommandError('Logging revision 0 is invalid.')
 
971
        if start_revno > end_revno:
 
972
            raise errors.BzrCommandError("Start revision must be older than "
 
973
                                         "the end revision.")
1110
974
    return (start_rev_id, end_rev_id)
1111
975
 
1112
976
 
1160
1024
            end_revno = end_revision
1161
1025
 
1162
1026
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1163
 
            or (end_rev_id == _mod_revision.NULL_REVISION)):
1164
 
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1027
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1028
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
1165
1029
    if start_revno > end_revno:
1166
 
        raise errors.BzrCommandError(gettext("Start revision must be older "
1167
 
                                             "than the end revision."))
 
1030
        raise errors.BzrCommandError("Start revision must be older than "
 
1031
                                     "the end revision.")
1168
1032
 
1169
1033
    if end_revno < start_revno:
1170
1034
        return None, None, None, None
1171
1035
    cur_revno = branch_revno
1172
1036
    rev_nos = {}
1173
1037
    mainline_revs = []
1174
 
    graph = branch.repository.get_graph()
1175
 
    for revision_id in graph.iter_lefthand_ancestry(
1176
 
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1038
    for revision_id in branch.repository.iter_reverse_revision_history(
 
1039
                        branch_last_revision):
1177
1040
        if cur_revno < start_revno:
1178
1041
            # We have gone far enough, but we always add 1 more revision
1179
1042
            rev_nos[revision_id] = cur_revno
1193
1056
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1194
1057
 
1195
1058
 
 
1059
@deprecated_function(deprecated_in((2, 2, 0)))
 
1060
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
 
1061
    """Filter view_revisions based on revision ranges.
 
1062
 
 
1063
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
 
1064
            tuples to be filtered.
 
1065
 
 
1066
    :param start_rev_id: If not NONE specifies the first revision to be logged.
 
1067
            If NONE then all revisions up to the end_rev_id are logged.
 
1068
 
 
1069
    :param end_rev_id: If not NONE specifies the last revision to be logged.
 
1070
            If NONE then all revisions up to the end of the log are logged.
 
1071
 
 
1072
    :return: The filtered view_revisions.
 
1073
    """
 
1074
    if start_rev_id or end_rev_id:
 
1075
        revision_ids = [r for r, n, d in view_revisions]
 
1076
        if start_rev_id:
 
1077
            start_index = revision_ids.index(start_rev_id)
 
1078
        else:
 
1079
            start_index = 0
 
1080
        if start_rev_id == end_rev_id:
 
1081
            end_index = start_index
 
1082
        else:
 
1083
            if end_rev_id:
 
1084
                end_index = revision_ids.index(end_rev_id)
 
1085
            else:
 
1086
                end_index = len(view_revisions) - 1
 
1087
        # To include the revisions merged into the last revision,
 
1088
        # extend end_rev_id down to, but not including, the next rev
 
1089
        # with the same or lesser merge_depth
 
1090
        end_merge_depth = view_revisions[end_index][2]
 
1091
        try:
 
1092
            for index in xrange(end_index+1, len(view_revisions)+1):
 
1093
                if view_revisions[index][2] <= end_merge_depth:
 
1094
                    end_index = index - 1
 
1095
                    break
 
1096
        except IndexError:
 
1097
            # if the search falls off the end then log to the end as well
 
1098
            end_index = len(view_revisions) - 1
 
1099
        view_revisions = view_revisions[start_index:end_index+1]
 
1100
    return view_revisions
 
1101
 
 
1102
 
1196
1103
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1197
 
                                       include_merges=True):
 
1104
    include_merges=True):
1198
1105
    r"""Return the list of revision ids which touch a given file id.
1199
1106
 
1200
1107
    The function filters view_revisions and returns a subset.
1201
1108
    This includes the revisions which directly change the file id,
1202
1109
    and the revisions which merge these changes. So if the
1203
1110
    revision graph is::
1204
 
 
1205
1111
        A-.
1206
1112
        |\ \
1207
1113
        B C E
1234
1140
    """
1235
1141
    # Lookup all possible text keys to determine which ones actually modified
1236
1142
    # the file.
1237
 
    graph = branch.repository.get_file_graph()
1238
 
    get_parent_map = graph.get_parent_map
1239
1143
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1240
1144
    next_keys = None
1241
1145
    # Looking up keys in batches of 1000 can cut the time in half, as well as
1245
1149
    #       indexing layer. We might consider passing in hints as to the known
1246
1150
    #       access pattern (sparse/clustered, high success rate/low success
1247
1151
    #       rate). This particular access is clustered with a low success rate.
 
1152
    get_parent_map = branch.repository.texts.get_parent_map
1248
1153
    modified_text_revisions = set()
1249
1154
    chunk_size = 1000
1250
 
    for start in range(0, len(text_keys), chunk_size):
 
1155
    for start in xrange(0, len(text_keys), chunk_size):
1251
1156
        next_keys = text_keys[start:start + chunk_size]
1252
1157
        # Only keep the revision_id portion of the key
1253
1158
        modified_text_revisions.update(
1268
1173
 
1269
1174
        if rev_id in modified_text_revisions:
1270
1175
            # This needs to be logged, along with the extra revisions
1271
 
            for idx in range(len(current_merge_stack)):
 
1176
            for idx in xrange(len(current_merge_stack)):
1272
1177
                node = current_merge_stack[idx]
1273
1178
                if node is not None:
1274
1179
                    if include_merges or node[2] == 0:
1277
1182
    return result
1278
1183
 
1279
1184
 
 
1185
@deprecated_function(deprecated_in((2, 2, 0)))
 
1186
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
 
1187
                       include_merges=True):
 
1188
    """Produce an iterator of revisions to show
 
1189
    :return: an iterator of (revision_id, revno, merge_depth)
 
1190
    (if there is no revno for a revision, None is supplied)
 
1191
    """
 
1192
    if not include_merges:
 
1193
        revision_ids = mainline_revs[1:]
 
1194
        if direction == 'reverse':
 
1195
            revision_ids.reverse()
 
1196
        for revision_id in revision_ids:
 
1197
            yield revision_id, str(rev_nos[revision_id]), 0
 
1198
        return
 
1199
    graph = branch.repository.get_graph()
 
1200
    # This asks for all mainline revisions, which means we only have to spider
 
1201
    # sideways, rather than depth history. That said, its still size-of-history
 
1202
    # and should be addressed.
 
1203
    # mainline_revisions always includes an extra revision at the beginning, so
 
1204
    # don't request it.
 
1205
    parent_map = dict(((key, value) for key, value in
 
1206
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
 
1207
    # filter out ghosts; merge_sort errors on ghosts.
 
1208
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
 
1209
    merge_sorted_revisions = tsort.merge_sort(
 
1210
        rev_graph,
 
1211
        mainline_revs[-1],
 
1212
        mainline_revs,
 
1213
        generate_revno=True)
 
1214
 
 
1215
    if direction == 'forward':
 
1216
        # forward means oldest first.
 
1217
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
 
1218
    elif direction != 'reverse':
 
1219
        raise ValueError('invalid direction %r' % direction)
 
1220
 
 
1221
    for (sequence, rev_id, merge_depth, revno, end_of_merge
 
1222
         ) in merge_sorted_revisions:
 
1223
        yield rev_id, '.'.join(map(str, revno)), merge_depth
 
1224
 
 
1225
 
1280
1226
def reverse_by_depth(merge_sorted_revisions, _depth=0):
1281
1227
    """Reverse revisions by depth.
1282
1228
 
1283
1229
    Revisions with a different depth are sorted as a group with the previous
1284
 
    revision of that depth.  There may be no topological justification for this
 
1230
    revision of that depth.  There may be no topological justification for this,
1285
1231
    but it looks much nicer.
1286
1232
    """
1287
1233
    # Add a fake revision at start so that we can always attach sub revisions
1317
1263
    """
1318
1264
 
1319
1265
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
1320
 
                 tags=None, diff=None, signature=None):
 
1266
                 tags=None, diff=None):
1321
1267
        self.rev = rev
1322
 
        if revno is None:
1323
 
            self.revno = None
1324
 
        else:
1325
 
            self.revno = str(revno)
 
1268
        self.revno = str(revno)
1326
1269
        self.merge_depth = merge_depth
1327
1270
        self.delta = delta
1328
1271
        self.tags = tags
1329
1272
        self.diff = diff
1330
 
        self.signature = signature
1331
1273
 
1332
1274
 
1333
1275
class LogFormatter(object):
1342
1284
    to indicate which LogRevision attributes it supports:
1343
1285
 
1344
1286
    - supports_delta must be True if this log formatter supports delta.
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.
 
1287
        Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1288
        attribute describes whether the 'short_status' format (1) or the long
 
1289
        one (2) should be used.
1348
1290
 
1349
1291
    - supports_merge_revisions must be True if this log formatter supports
1350
 
      merge revisions.  If not, then only mainline revisions will be passed
1351
 
      to the formatter.
 
1292
        merge revisions.  If not, then only mainline revisions will be passed
 
1293
        to the formatter.
1352
1294
 
1353
1295
    - preferred_levels is the number of levels this formatter defaults to.
1354
 
      The default value is zero meaning display all levels.
1355
 
      This value is only relevant if supports_merge_revisions is True.
 
1296
        The default value is zero meaning display all levels.
 
1297
        This value is only relevant if supports_merge_revisions is True.
1356
1298
 
1357
1299
    - supports_tags must be True if this log formatter supports tags.
1358
 
      Otherwise the tags attribute may not be populated.
 
1300
        Otherwise the tags attribute may not be populated.
1359
1301
 
1360
1302
    - supports_diff must be True if this log formatter supports diffs.
1361
 
      Otherwise the diff attribute may not be populated.
1362
 
 
1363
 
    - supports_signatures must be True if this log formatter supports GPG
1364
 
      signatures.
 
1303
        Otherwise the diff attribute may not be populated.
1365
1304
 
1366
1305
    Plugins can register functions to show custom revision properties using
1367
1306
    the properties_handler_registry. The registered function
1368
 
    must respect the following interface description::
1369
 
 
 
1307
    must respect the following interface description:
1370
1308
        def my_show_properties(properties_dict):
1371
1309
            # code that returns a dict {'name':'value'} of the properties
1372
1310
            # to be shown
1375
1313
 
1376
1314
    def __init__(self, to_file, show_ids=False, show_timezone='original',
1377
1315
                 delta_format=None, levels=None, show_advice=False,
1378
 
                 to_exact_file=None, author_list_handler=None):
 
1316
                 to_exact_file=None):
1379
1317
        """Create a LogFormatter.
1380
1318
 
1381
1319
        :param to_file: the file to output to
1382
 
        :param to_exact_file: if set, gives an output stream to which
 
1320
        :param to_exact_file: if set, gives an output stream to which 
1383
1321
             non-Unicode diffs are written.
1384
1322
        :param show_ids: if True, revision-ids are to be displayed
1385
1323
        :param show_timezone: the timezone to use
1389
1327
          let the log formatter decide.
1390
1328
        :param show_advice: whether to show advice at the end of the
1391
1329
          log or not
1392
 
        :param author_list_handler: callable generating a list of
1393
 
          authors to display for a given revision
1394
1330
        """
1395
1331
        self.to_file = to_file
1396
1332
        # 'exact' stream used to show diff, it should print content 'as is'
1397
 
        # and should not try to decode/encode it to unicode to avoid bug
1398
 
        # #328007
 
1333
        # and should not try to decode/encode it to unicode to avoid bug #328007
1399
1334
        if to_exact_file is not None:
1400
1335
            self.to_exact_file = to_exact_file
1401
1336
        else:
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
 
1337
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1338
            # for code that expects to get diffs to pass in the exact file
 
1339
            # stream
1405
1340
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1406
1341
        self.show_ids = show_ids
1407
1342
        self.show_timezone = show_timezone
1408
1343
        if delta_format is None:
1409
1344
            # Ensures backward compatibility
1410
 
            delta_format = 2  # long format
 
1345
            delta_format = 2 # long format
1411
1346
        self.delta_format = delta_format
1412
1347
        self.levels = levels
1413
1348
        self._show_advice = show_advice
1414
1349
        self._merge_count = 0
1415
 
        self._author_list_handler = author_list_handler
1416
1350
 
1417
1351
    def get_levels(self):
1418
1352
        """Get the number of levels to display or 0 for all."""
1437
1371
            if advice_sep:
1438
1372
                self.to_file.write(advice_sep)
1439
1373
            self.to_file.write(
1440
 
                "Use --include-merged or -n0 to see merged revisions.\n")
 
1374
                "Use --include-merges or -n0 to see merged revisions.\n")
1441
1375
 
1442
1376
    def get_advice_separator(self):
1443
1377
        """Get the text separating the log from the closing advice."""
1450
1384
        return address
1451
1385
 
1452
1386
    def short_author(self, rev):
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
 
1387
        name, address = config.parse_username(rev.get_apparent_authors()[0])
 
1388
        if name:
 
1389
            return name
 
1390
        return address
1488
1391
 
1489
1392
    def merge_marker(self, revision):
1490
1393
        """Get the merge marker to include in the output or '' if none."""
1511
1414
        """
1512
1415
        lines = self._foreign_info_properties(revision)
1513
1416
        for key, handler in properties_handler_registry.iteritems():
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)
 
1417
            lines.extend(self._format_properties(handler(revision)))
1519
1418
        return lines
1520
1419
 
1521
1420
    def _foreign_info_properties(self, rev):
1525
1424
        """
1526
1425
        # Revision comes directly from a foreign repository
1527
1426
        if isinstance(rev, foreign.ForeignRevision):
1528
 
            return self._format_properties(
1529
 
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1427
            return rev.mapping.vcs.show_foreign_revid(rev.foreign_revid)
1530
1428
 
1531
1429
        # Imported foreign revision revision ids always contain :
1532
 
        if b":" not in rev.revision_id:
 
1430
        if not ":" in rev.revision_id:
1533
1431
            return []
1534
1432
 
1535
1433
        # Revision was once imported from a foreign repository
1549
1447
        return lines
1550
1448
 
1551
1449
    def show_diff(self, to_file, diff, indent):
1552
 
        encoding = get_terminal_encoding()
1553
 
        for l in diff.rstrip().split(b'\n'):
1554
 
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
 
1450
        for l in diff.rstrip().split('\n'):
 
1451
            to_file.write(indent + '%s\n' % (l,))
1555
1452
 
1556
1453
 
1557
1454
# Separator between revisions in long format
1565
1462
    supports_delta = True
1566
1463
    supports_tags = True
1567
1464
    supports_diff = True
1568
 
    supports_signatures = True
1569
1465
 
1570
1466
    def __init__(self, *args, **kwargs):
1571
1467
        super(LongLogFormatter, self).__init__(*args, **kwargs)
1580
1476
 
1581
1477
    def _date_string_original_timezone(self, rev):
1582
1478
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1583
 
                                                            rev.timezone or 0)
 
1479
            rev.timezone or 0)
1584
1480
 
1585
1481
    def log_revision(self, revision):
1586
1482
        """Log a revision, either merged or not."""
1588
1484
        lines = [_LONG_SEP]
1589
1485
        if revision.revno is not None:
1590
1486
            lines.append('revno: %s%s' % (revision.revno,
1591
 
                                          self.merge_marker(revision)))
 
1487
                self.merge_marker(revision)))
1592
1488
        if 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'),))
 
1489
            lines.append('tags: %s' % (', '.join(revision.tags)))
1597
1490
        if self.show_ids:
 
1491
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1598
1492
            for parent_id in revision.rev.parent_ids:
1599
 
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1493
                lines.append('parent: %s' % (parent_id,))
1600
1494
        lines.extend(self.custom_properties(revision.rev))
1601
1495
 
1602
1496
        committer = revision.rev.committer
1603
 
        authors = self.authors(revision.rev, 'all')
 
1497
        authors = revision.rev.get_apparent_authors()
1604
1498
        if authors != [committer]:
1605
1499
            lines.append('author: %s' % (", ".join(authors),))
1606
1500
        lines.append('committer: %s' % (committer,))
1611
1505
 
1612
1506
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
1613
1507
 
1614
 
        if revision.signature is not None:
1615
 
            lines.append('signature: ' + revision.signature)
1616
 
 
1617
1508
        lines.append('message:')
1618
1509
        if not revision.rev.message:
1619
1510
            lines.append('  (no message)')
1626
1517
        to_file = self.to_file
1627
1518
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1628
1519
        if revision.delta is not None:
1629
 
            # Use the standard status output to display changes
1630
 
            from breezy.delta import report_delta
1631
 
            report_delta(to_file, revision.delta, short_status=False,
1632
 
                         show_ids=self.show_ids, indent=indent)
 
1520
            # We don't respect delta_format for compatibility
 
1521
            revision.delta.show(to_file, self.show_ids, indent=indent,
 
1522
                                short_status=False)
1633
1523
        if revision.diff is not None:
1634
1524
            to_file.write(indent + 'diff:\n')
1635
1525
            to_file.flush()
1666
1556
        indent = '    ' * depth
1667
1557
        revno_width = self.revno_width_by_depth.get(depth)
1668
1558
        if revno_width is None:
1669
 
            if revision.revno is None or revision.revno.find('.') == -1:
 
1559
            if revision.revno.find('.') == -1:
1670
1560
                # mainline revno, e.g. 12345
1671
1561
                revno_width = 5
1672
1562
            else:
1678
1568
        to_file = self.to_file
1679
1569
        tags = ''
1680
1570
        if revision.tags:
1681
 
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1571
            tags = ' {%s}' % (', '.join(revision.tags))
1682
1572
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
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:
 
1573
                revision.revno, self.short_author(revision.rev),
 
1574
                format_date(revision.rev.timestamp,
 
1575
                            revision.rev.timezone or 0,
 
1576
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
1577
                            show_offset=False),
 
1578
                tags, self.merge_marker(revision)))
 
1579
        self.show_properties(revision.rev, indent+offset)
 
1580
        if self.show_ids:
1692
1581
            to_file.write(indent + offset + 'revision-id:%s\n'
1693
 
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1582
                          % (revision.rev.revision_id,))
1694
1583
        if not revision.rev.message:
1695
1584
            to_file.write(indent + offset + '(no message)\n')
1696
1585
        else:
1699
1588
                to_file.write(indent + offset + '%s\n' % (l,))
1700
1589
 
1701
1590
        if revision.delta is not None:
1702
 
            # Use the standard status output to display changes
1703
 
            from breezy.delta import report_delta
1704
 
            report_delta(to_file, revision.delta,
1705
 
                         short_status=self.delta_format == 1,
1706
 
                         show_ids=self.show_ids, indent=indent + offset)
 
1591
            revision.delta.show(to_file, self.show_ids, indent=indent + offset,
 
1592
                                short_status=self.delta_format==1)
1707
1593
        if revision.diff is not None:
1708
1594
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1709
1595
        to_file.write('\n')
1726
1612
    def truncate(self, str, max_len):
1727
1613
        if max_len is None or len(str) <= max_len:
1728
1614
            return str
1729
 
        return str[:max_len - 3] + '...'
 
1615
        return str[:max_len-3] + '...'
1730
1616
 
1731
1617
    def date_string(self, rev):
1732
1618
        return format_date(rev.timestamp, rev.timezone or 0,
1742
1628
    def log_revision(self, revision):
1743
1629
        indent = '  ' * revision.merge_depth
1744
1630
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1745
 
                                           self._max_chars, revision.tags, indent))
 
1631
            self._max_chars, revision.tags, indent))
1746
1632
        self.to_file.write('\n')
1747
1633
 
1748
1634
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1749
1635
        """Format log info into one string. Truncate tail of 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
 
1636
        :param  revno:      revision number or None.
 
1637
                            Revision numbers counts from 1.
 
1638
        :param  rev:        revision object
 
1639
        :param  max_chars:  maximum length of resulting string
 
1640
        :param  tags:       list of tags or None
 
1641
        :param  prefix:     string to prefix each line
 
1642
        :return:            formatted truncated string
1758
1643
        """
1759
1644
        out = []
1760
1645
        if revno:
1761
1646
            # show revno only when is not None
1762
1647
            out.append("%s:" % revno)
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))
 
1648
        out.append(self.truncate(self.short_author(rev), 20))
1768
1649
        out.append(self.date_string(rev))
1769
1650
        if len(rev.parent_ids) > 1:
1770
1651
            out.append('[merge]')
1771
1652
        if tags:
1772
 
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1653
            tag_str = '{%s}' % (', '.join(tags))
1773
1654
            out.append(tag_str)
1774
1655
        out.append(rev.get_summary())
1775
1656
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1789
1670
                               self.show_timezone,
1790
1671
                               date_fmt='%Y-%m-%d',
1791
1672
                               show_offset=False)
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))
 
1673
        committer_str = revision.rev.committer.replace (' <', '  <')
 
1674
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1795
1675
 
1796
1676
        if revision.delta is not None and revision.delta.has_changed():
1797
1677
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1798
 
                if c.path[0] is None:
1799
 
                    path = c.path[1]
1800
 
                else:
1801
 
                    path = c.path[0]
 
1678
                path, = c[:1]
1802
1679
                to_file.write('\t* %s:\n' % (path,))
1803
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1680
            for c in revision.delta.renamed:
 
1681
                oldpath,newpath = c[:2]
1804
1682
                # For renamed files, show both the old and the new path
1805
 
                to_file.write('\t* %s:\n\t* %s:\n' % (c.path[0], c.path[1]))
 
1683
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
1806
1684
            to_file.write('\n')
1807
1685
 
1808
1686
        if not revision.rev.message:
1831
1709
        return self.get(name)(*args, **kwargs)
1832
1710
 
1833
1711
    def get_default(self, branch):
1834
 
        c = branch.get_config_stack()
1835
 
        return self.get(c.get('log_format'))
 
1712
        return self.get(branch.get_config().log_format())
1836
1713
 
1837
1714
 
1838
1715
log_formatter_registry = LogFormatterRegistry()
1839
1716
 
1840
1717
 
1841
1718
log_formatter_registry.register('short', ShortLogFormatter,
1842
 
                                'Moderately short log format.')
 
1719
                                'Moderately short log format')
1843
1720
log_formatter_registry.register('long', LongLogFormatter,
1844
 
                                'Detailed log format.')
 
1721
                                'Detailed log format')
1845
1722
log_formatter_registry.register('line', LineLogFormatter,
1846
 
                                'Log format with one line per revision.')
 
1723
                                'Log format with one line per revision')
1847
1724
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
1848
 
                                'Format used by GNU ChangeLog files.')
 
1725
                                'Format used by GNU ChangeLog files')
1849
1726
 
1850
1727
 
1851
1728
def register_formatter(name, formatter):
1861
1738
    try:
1862
1739
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1863
1740
    except KeyError:
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')
 
1741
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
 
1742
 
 
1743
 
 
1744
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
 
1745
    # deprecated; for compatibility
 
1746
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
 
1747
    lf.show(revno, rev, delta)
1894
1748
 
1895
1749
 
1896
1750
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1904
1758
    """
1905
1759
    if to_file is None:
1906
1760
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1907
 
                                                            errors='replace')
 
1761
            errors='replace')
1908
1762
    lf = log_formatter(log_format,
1909
1763
                       show_ids=False,
1910
1764
                       to_file=to_file,
1913
1767
    # This is the first index which is different between
1914
1768
    # old and new
1915
1769
    base_idx = None
1916
 
    for i in range(max(len(new_rh), len(old_rh))):
 
1770
    for i in xrange(max(len(new_rh),
 
1771
                        len(old_rh))):
1917
1772
        if (len(new_rh) <= i
1918
1773
            or len(old_rh) <= i
1919
 
                or new_rh[i] != old_rh[i]):
 
1774
            or new_rh[i] != old_rh[i]):
1920
1775
            base_idx = i
1921
1776
            break
1922
1777
 
1923
1778
    if base_idx is None:
1924
1779
        to_file.write('Nothing seems to have changed\n')
1925
1780
        return
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
 
1781
    ## TODO: It might be nice to do something like show_log
 
1782
    ##       and show the merged entries. But since this is the
 
1783
    ##       removed revisions, it shouldn't be as important
1929
1784
    if base_idx < len(old_rh):
1930
 
        to_file.write('*' * 60)
 
1785
        to_file.write('*'*60)
1931
1786
        to_file.write('\nRemoved Revisions:\n')
1932
1787
        for i in range(base_idx, len(old_rh)):
1933
1788
            rev = branch.repository.get_revision(old_rh[i])
1934
 
            lr = LogRevision(rev, i + 1, 0, None)
 
1789
            lr = LogRevision(rev, i+1, 0, None)
1935
1790
            lf.log_revision(lr)
1936
 
        to_file.write('*' * 60)
 
1791
        to_file.write('*'*60)
1937
1792
        to_file.write('\n\n')
1938
1793
    if base_idx < len(new_rh):
1939
1794
        to_file.write('Added Revisions:\n')
1940
1795
        show_log(branch,
1941
1796
                 lf,
 
1797
                 None,
1942
1798
                 verbose=False,
1943
1799
                 direction='forward',
1944
 
                 start_revision=base_idx + 1,
 
1800
                 start_revision=base_idx+1,
1945
1801
                 end_revision=len(new_rh),
1946
1802
                 search=None)
1947
1803
 
1959
1815
    old_revisions = set()
1960
1816
    new_history = []
1961
1817
    new_revisions = set()
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)
 
1818
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
 
1819
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
1965
1820
    stop_revision = None
1966
1821
    do_old = True
1967
1822
    do_new = True
1968
1823
    while do_new or do_old:
1969
1824
        if do_new:
1970
1825
            try:
1971
 
                new_revision = next(new_iter)
 
1826
                new_revision = new_iter.next()
1972
1827
            except StopIteration:
1973
1828
                do_new = False
1974
1829
            else:
1979
1834
                    break
1980
1835
        if do_old:
1981
1836
            try:
1982
 
                old_revision = next(old_iter)
 
1837
                old_revision = old_iter.next()
1983
1838
            except StopIteration:
1984
1839
                do_old = False
1985
1840
            else:
2015
1870
    log_format = log_formatter_registry.get_default(branch)
2016
1871
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
2017
1872
    if old_history != []:
2018
 
        output.write('*' * 60)
 
1873
        output.write('*'*60)
2019
1874
        output.write('\nRemoved Revisions:\n')
2020
1875
        show_flat_log(branch.repository, old_history, old_revno, lf)
2021
 
        output.write('*' * 60)
 
1876
        output.write('*'*60)
2022
1877
        output.write('\n\n')
2023
1878
    if new_history != []:
2024
1879
        output.write('Added Revisions:\n')
2025
1880
        start_revno = new_revno - len(new_history) + 1
2026
 
        show_log(branch, lf, verbose=False, direction='forward',
2027
 
                 start_revision=start_revno)
 
1881
        show_log(branch, lf, None, verbose=False, direction='forward',
 
1882
                 start_revision=start_revno,)
2028
1883
 
2029
1884
 
2030
1885
def show_flat_log(repository, history, last_revno, lf):
2035
1890
    :param last_revno: The revno of the last revision_id in the history.
2036
1891
    :param lf: The log formatter to use.
2037
1892
    """
 
1893
    start_revno = last_revno - len(history) + 1
2038
1894
    revisions = repository.get_revisions(history)
2039
1895
    for i, rev in enumerate(revisions):
2040
1896
        lr = LogRevision(rev, i + last_revno, 0, None)
2041
1897
        lf.log_revision(lr)
2042
1898
 
2043
1899
 
2044
 
def _get_info_for_log_files(revisionspec_list, file_list, exit_stack):
 
1900
def _get_info_for_log_files(revisionspec_list, file_list):
2045
1901
    """Find file-ids and kinds given a list of files and a revision range.
2046
1902
 
2047
1903
    We search for files at the end of the range. If not found there,
2051
1907
    :param file_list: the list of paths given on the command line;
2052
1908
      the first of these can be a branch location or a file path,
2053
1909
      the remainder must be file paths
2054
 
    :param exit_stack: When the branch returned is read locked,
2055
 
      an unlock call will be queued to the exit stack.
2056
1910
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2057
1911
      info_list is a list of (relative_path, file_id, kind) tuples where
2058
1912
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2059
1913
      branch will be read-locked.
2060
1914
    """
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())
 
1915
    from builtins import _get_revision_range, safe_relpath_files
 
1916
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
1917
    b.lock_read()
2065
1918
    # XXX: It's damn messy converting a list of paths to relative paths when
2066
1919
    # those paths might be deleted ones, they might be on a case-insensitive
2067
1920
    # filesystem and/or they might be in silly locations (like another branch).
2071
1924
    # case of running log in a nested directory, assuming paths beyond the
2072
1925
    # first one haven't been deleted ...
2073
1926
    if tree:
2074
 
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
1927
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
2075
1928
    else:
2076
1929
        relpaths = [path] + file_list[1:]
2077
1930
    info_list = []
2078
1931
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2079
 
                                                       "log")
 
1932
        "log")
2080
1933
    if relpaths in ([], [u'']):
2081
1934
        return b, [], start_rev_info, end_rev_info
2082
1935
    if start_rev_info is None and end_rev_info is None:
2085
1938
        tree1 = None
2086
1939
        for fp in relpaths:
2087
1940
            file_id = tree.path2id(fp)
2088
 
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
1941
            kind = _get_kind_for_file_id(tree, file_id)
2089
1942
            if file_id is None:
2090
1943
                # go back to when time began
2091
1944
                if tree1 is None:
2099
1952
                        tree1 = b.repository.revision_tree(rev1)
2100
1953
                if tree1:
2101
1954
                    file_id = tree1.path2id(fp)
2102
 
                    kind = _get_kind_for_file_id(tree1, fp, file_id)
 
1955
                    kind = _get_kind_for_file_id(tree1, file_id)
2103
1956
            info_list.append((fp, file_id, kind))
2104
1957
 
2105
1958
    elif start_rev_info == end_rev_info:
2107
1960
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2108
1961
        for fp in relpaths:
2109
1962
            file_id = tree.path2id(fp)
2110
 
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
1963
            kind = _get_kind_for_file_id(tree, file_id)
2111
1964
            info_list.append((fp, file_id, kind))
2112
1965
 
2113
1966
    else:
2121
1974
        tree1 = None
2122
1975
        for fp in relpaths:
2123
1976
            file_id = tree.path2id(fp)
2124
 
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
1977
            kind = _get_kind_for_file_id(tree, file_id)
2125
1978
            if file_id is None:
2126
1979
                if tree1 is None:
2127
1980
                    rev_id = start_rev_info.rev_id
2131
1984
                    else:
2132
1985
                        tree1 = b.repository.revision_tree(rev_id)
2133
1986
                file_id = tree1.path2id(fp)
2134
 
                kind = _get_kind_for_file_id(tree1, fp, file_id)
 
1987
                kind = _get_kind_for_file_id(tree1, file_id)
2135
1988
            info_list.append((fp, file_id, kind))
2136
1989
    return b, info_list, start_rev_info, end_rev_info
2137
1990
 
2138
1991
 
2139
 
def _get_kind_for_file_id(tree, path, file_id):
 
1992
def _get_kind_for_file_id(tree, file_id):
2140
1993
    """Return the kind of a file-id or None if it doesn't exist."""
2141
1994
    if file_id is not None:
2142
 
        return tree.kind(path)
 
1995
        return tree.kind(file_id)
2143
1996
    else:
2144
1997
        return None
2145
1998
 
2147
2000
properties_handler_registry = registry.Registry()
2148
2001
 
2149
2002
# Use the properties handlers to print out bug information if available
2150
 
 
2151
 
 
2152
2003
def _bugs_properties_handler(revision):
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
2169
 
 
 
2004
    if revision.properties.has_key('bugs'):
 
2005
        bug_lines = revision.properties['bugs'].split('\n')
 
2006
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2007
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2008
                          len(row) > 1 and row[1] == 'fixed']
 
2009
        
 
2010
        if fixed_bug_urls:
 
2011
            return {'fixes bug(s)': ' '.join(fixed_bug_urls)}
 
2012
    return {}
2170
2013
 
2171
2014
properties_handler_registry.register('bugs_properties_handler',
2172
2015
                                     _bugs_properties_handler)