/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: Richard Wilbur
  • Date: 2016-02-04 19:07:28 UTC
  • mto: This revision was merged to the branch mainline in revision 6618.
  • Revision ID: richard.wilbur@gmail.com-20160204190728-p0zvfii6zase0fw7
Update COPYING.txt from the original http://www.gnu.org/licenses/gpl-2.0.txt  (Only differences were in whitespace.)  Thanks to Petr Stodulka for pointing out the discrepancy.

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
* in "verbose" mode with a description of what changed from one
25
25
  version to the next
26
26
 
27
 
* with files and revision-ids shown
 
27
* with file-ids and revision-ids shown
28
28
 
29
29
Logs are actually written out through an abstract LogFormatter
30
30
interface, which allows for different preferred formats.  Plugins can
47
47
all the changes since the previous revision that touched hello.c.
48
48
"""
49
49
 
 
50
from __future__ import absolute_import
 
51
 
50
52
import codecs
51
 
from io import BytesIO
52
 
import itertools
 
53
from cStringIO import StringIO
 
54
from itertools import (
 
55
    chain,
 
56
    izip,
 
57
    )
53
58
import re
54
59
import sys
55
60
from warnings import (
56
61
    warn,
57
62
    )
58
63
 
59
 
from .lazy_import import lazy_import
 
64
from bzrlib.lazy_import import lazy_import
60
65
lazy_import(globals(), """
61
66
 
62
 
from breezy import (
 
67
from bzrlib import (
63
68
    config,
64
69
    controldir,
65
70
    diff,
 
71
    errors,
66
72
    foreign,
67
 
    lazy_regex,
 
73
    repository as _mod_repository,
68
74
    revision as _mod_revision,
 
75
    revisionspec,
 
76
    tsort,
69
77
    )
70
 
from breezy.i18n import gettext, ngettext
 
78
from bzrlib.i18n import gettext, ngettext
71
79
""")
72
80
 
73
 
from . import (
74
 
    errors,
 
81
from bzrlib import (
 
82
    lazy_regex,
75
83
    registry,
76
 
    revisionspec,
77
 
    trace,
78
84
    )
79
 
from .osutils import (
 
85
from bzrlib.osutils import (
80
86
    format_date,
81
87
    format_date_with_offset_in_original_timezone,
82
88
    get_diff_header_encoding,
83
89
    get_terminal_encoding,
84
 
    is_inside,
85
90
    terminal_width,
86
91
    )
87
 
from .tree import (
88
 
    find_previous_path,
89
 
    InterTree,
90
 
    )
91
 
 
92
 
 
93
 
def find_touching_revisions(repository, last_revision, last_tree, last_path):
94
 
    """Yield a description of revisions which affect the file.
 
92
 
 
93
 
 
94
def find_touching_revisions(branch, file_id):
 
95
    """Yield a description of revisions which affect the file_id.
95
96
 
96
97
    Each returned element is (revno, revision_id, description)
97
98
 
101
102
    TODO: Perhaps some way to limit this to only particular revisions,
102
103
    or to traverse a non-mainline set of revisions?
103
104
    """
104
 
    last_verifier = last_tree.get_file_verifier(last_path)
105
 
    graph = repository.get_graph()
106
 
    history = list(graph.iter_lefthand_ancestry(last_revision, []))
107
 
    revno = len(history)
108
 
    for revision_id in history:
109
 
        this_tree = repository.revision_tree(revision_id)
110
 
        this_intertree = InterTree.get(this_tree, last_tree)
111
 
        this_path = this_intertree.find_source_path(last_path)
 
105
    last_ie = None
 
106
    last_path = None
 
107
    revno = 1
 
108
    graph = branch.repository.get_graph()
 
109
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
110
        [_mod_revision.NULL_REVISION]))
 
111
    for revision_id in reversed(history):
 
112
        this_inv = branch.repository.get_inventory(revision_id)
 
113
        if this_inv.has_id(file_id):
 
114
            this_ie = this_inv[file_id]
 
115
            this_path = this_inv.id2path(file_id)
 
116
        else:
 
117
            this_ie = this_path = None
112
118
 
113
119
        # now we know how it was last time, and how it is in this revision.
114
120
        # are those two states effectively the same or not?
115
 
        if this_path is not None and last_path is None:
116
 
            yield revno, revision_id, "deleted " + this_path
117
 
            this_verifier = this_tree.get_file_verifier(this_path)
118
 
        elif this_path is None and last_path is not None:
119
 
            yield revno, revision_id, "added " + last_path
 
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
120
130
        elif this_path != last_path:
121
 
            yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
122
 
            this_verifier = this_tree.get_file_verifier(this_path)
123
 
        else:
124
 
            this_verifier = this_tree.get_file_verifier(this_path)
125
 
            if (this_verifier != last_verifier):
126
 
                yield revno, revision_id, "modified " + this_path
 
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
127
135
 
128
 
        last_verifier = this_verifier
 
136
        last_ie = this_ie
129
137
        last_path = this_path
130
 
        last_tree = this_tree
131
 
        if last_path is None:
132
 
            return
133
 
        revno -= 1
 
138
        revno += 1
134
139
 
135
140
 
136
141
def show_log(branch,
137
142
             lf,
 
143
             specific_fileid=None,
138
144
             verbose=False,
139
145
             direction='reverse',
140
146
             start_revision=None,
141
147
             end_revision=None,
 
148
             search=None,
142
149
             limit=None,
143
150
             show_diff=False,
144
151
             match=None):
151
158
 
152
159
    :param lf: The LogFormatter object showing the output.
153
160
 
 
161
    :param specific_fileid: If not None, list only the commits affecting the
 
162
        specified file, rather than all commits.
 
163
 
154
164
    :param verbose: If True show added/changed/deleted/renamed files.
155
165
 
156
166
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
160
170
 
161
171
    :param end_revision: If not None, only show revisions <= end_revision
162
172
 
 
173
    :param search: If not None, only show revisions with matching commit
 
174
        messages
 
175
 
163
176
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
164
177
        if None or 0.
165
178
 
168
181
    :param match: Dictionary of search lists to use when matching revision
169
182
      properties.
170
183
    """
 
184
    # Convert old-style parameters to new-style parameters
 
185
    if specific_fileid is not None:
 
186
        file_ids = [specific_fileid]
 
187
    else:
 
188
        file_ids = None
171
189
    if verbose:
172
 
        delta_type = 'full'
 
190
        if file_ids:
 
191
            delta_type = 'partial'
 
192
        else:
 
193
            delta_type = 'full'
173
194
    else:
174
195
        delta_type = None
175
196
    if show_diff:
176
 
        diff_type = 'full'
 
197
        if file_ids:
 
198
            diff_type = 'partial'
 
199
        else:
 
200
            diff_type = 'full'
177
201
    else:
178
202
        diff_type = None
179
203
 
180
 
    if isinstance(start_revision, int):
181
 
        try:
182
 
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
183
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
184
 
            raise errors.InvalidRevisionNumber(start_revision)
185
 
 
186
 
    if isinstance(end_revision, int):
187
 
        try:
188
 
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
189
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
190
 
            raise errors.InvalidRevisionNumber(end_revision)
191
 
 
192
 
    if end_revision is not None and end_revision.revno == 0:
193
 
        raise errors.InvalidRevisionNumber(end_revision.revno)
194
 
 
195
204
    # Build the request and execute it
196
 
    rqst = make_log_request_dict(
197
 
        direction=direction,
 
205
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
198
206
        start_revision=start_revision, end_revision=end_revision,
199
 
        limit=limit, delta_type=delta_type, diff_type=diff_type)
 
207
        limit=limit, message_search=search,
 
208
        delta_type=delta_type, diff_type=diff_type)
200
209
    Logger(branch, rqst).show(lf)
201
210
 
202
211
 
211
220
    }
212
221
 
213
222
 
214
 
def make_log_request_dict(direction='reverse', specific_files=None,
 
223
def make_log_request_dict(direction='reverse', specific_fileids=None,
215
224
                          start_revision=None, end_revision=None, limit=None,
216
225
                          message_search=None, levels=None, generate_tags=True,
217
226
                          delta_type=None,
228
237
    :param direction: 'reverse' (default) is latest to earliest;
229
238
      'forward' is earliest to latest.
230
239
 
231
 
    :param specific_files: If not None, only include revisions
 
240
    :param specific_fileids: If not None, only include revisions
232
241
      affecting the specified files, rather than all revisions.
233
242
 
234
243
    :param start_revision: If not None, only generate
251
260
`
252
261
    :param delta_type: Either 'full', 'partial' or None.
253
262
      'full' means generate the complete delta - adds/deletes/modifies/etc;
254
 
      'partial' means filter the delta using specific_files;
 
263
      'partial' means filter the delta using specific_fileids;
255
264
      None means do not generate any delta.
256
265
 
257
266
    :param diff_type: Either 'full', 'partial' or None.
258
267
      'full' means generate the complete diff - adds/deletes/modifies/etc;
259
 
      'partial' means filter the diff using specific_files;
 
268
      'partial' means filter the diff using specific_fileids;
260
269
      None means do not generate any diff.
261
270
 
262
271
    :param _match_using_deltas: a private parameter controlling the
263
 
      algorithm used for matching specific_files. This parameter
264
 
      may be removed in the future so breezy client code should NOT
 
272
      algorithm used for matching specific_fileids. This parameter
 
273
      may be removed in the future so bzrlib client code should NOT
265
274
      use it.
266
275
 
267
276
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
285
294
            else:
286
295
                match['message'] = [message_search]
287
296
        else:
288
 
            match = {'message': [message_search]}
 
297
            match={ 'message': [message_search] }
289
298
    return {
290
299
        'direction': direction,
291
 
        'specific_files': specific_files,
 
300
        'specific_fileids': specific_fileids,
292
301
        'start_revision': start_revision,
293
302
        'end_revision': end_revision,
294
303
        'limit': limit,
313
322
    return result
314
323
 
315
324
 
316
 
def format_signature_validity(rev_id, branch):
 
325
def format_signature_validity(rev_id, repo):
317
326
    """get the signature validity
318
327
 
319
328
    :param rev_id: revision id to validate
320
 
    :param branch: branch of revision
 
329
    :param repo: repository of revision
321
330
    :return: human readable string to print to log
322
331
    """
323
 
    from breezy import gpg
 
332
    from bzrlib import gpg
324
333
 
325
 
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
326
 
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
 
334
    gpg_strategy = gpg.GPGStrategy(None)
 
335
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
327
336
    if result[0] == gpg.SIGNATURE_VALID:
328
337
        return u"valid signature from {0}".format(result[1])
329
338
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
366
375
        if not isinstance(lf, LogFormatter):
367
376
            warn("not a LogFormatter instance: %r" % lf)
368
377
 
369
 
        with self.branch.lock_read():
 
378
        self.branch.lock_read()
 
379
        try:
370
380
            if getattr(lf, 'begin_log', None):
371
381
                lf.begin_log()
372
382
            self._show_body(lf)
373
383
            if getattr(lf, 'end_log', None):
374
384
                lf.end_log()
 
385
        finally:
 
386
            self.branch.unlock()
375
387
 
376
388
    def _show_body(self, lf):
377
389
        """Show the main log output.
396
408
 
397
409
        # Find and print the interesting revisions
398
410
        generator = self._generator_factory(self.branch, rqst)
399
 
        try:
400
 
            for lr in generator.iter_log_revisions():
401
 
                lf.log_revision(lr)
402
 
        except errors.GhostRevisionUnusableHere:
403
 
            raise errors.CommandError(
404
 
                gettext('Further revision history missing.'))
 
411
        for lr in generator.iter_log_revisions():
 
412
            lf.log_revision(lr)
405
413
        lf.show_advice()
406
414
 
407
415
    def _generator_factory(self, branch, rqst):
409
417
 
410
418
        Subclasses may wish to override this.
411
419
        """
412
 
        return _DefaultLogGenerator(branch, **rqst)
413
 
 
414
 
 
415
 
def _log_revision_iterator_using_per_file_graph(
416
 
        branch, delta_type, match, levels, path, start_rev_id, end_rev_id,
417
 
        direction, exclude_common_ancestry):
418
 
    # Get the base revisions, filtering by the revision range.
419
 
    # Note that we always generate the merge revisions because
420
 
    # filter_revisions_touching_path() requires them ...
421
 
    view_revisions = _calc_view_revisions(
422
 
        branch, start_rev_id, end_rev_id,
423
 
        direction, generate_merge_revisions=True,
424
 
        exclude_common_ancestry=exclude_common_ancestry)
425
 
    if not isinstance(view_revisions, list):
426
 
        view_revisions = list(view_revisions)
427
 
    view_revisions = _filter_revisions_touching_path(
428
 
        branch, path, view_revisions,
429
 
        include_merges=levels != 1)
430
 
    return make_log_rev_iterator(
431
 
        branch, view_revisions, delta_type, match)
432
 
 
433
 
 
434
 
def _log_revision_iterator_using_delta_matching(
435
 
        branch, delta_type, match, levels, specific_files, start_rev_id, end_rev_id,
436
 
        direction, exclude_common_ancestry, limit):
437
 
    # Get the base revisions, filtering by the revision range
438
 
    generate_merge_revisions = levels != 1
439
 
    delayed_graph_generation = not specific_files and (
440
 
        limit or start_rev_id or end_rev_id)
441
 
    view_revisions = _calc_view_revisions(
442
 
        branch, start_rev_id, end_rev_id,
443
 
        direction,
444
 
        generate_merge_revisions=generate_merge_revisions,
445
 
        delayed_graph_generation=delayed_graph_generation,
446
 
        exclude_common_ancestry=exclude_common_ancestry)
447
 
 
448
 
    # Apply the other filters
449
 
    return make_log_rev_iterator(branch, view_revisions,
450
 
                                 delta_type, match,
451
 
                                 files=specific_files,
452
 
                                 direction=direction)
453
 
 
454
 
 
455
 
def _format_diff(branch, rev, diff_type, files=None):
456
 
    """Format a diff.
457
 
 
458
 
    :param branch: Branch object
459
 
    :param rev: Revision object
460
 
    :param diff_type: Type of diff to generate
461
 
    :param files: List of files to generate diff for (or None for all)
462
 
    """
463
 
    repo = branch.repository
464
 
    if len(rev.parent_ids) == 0:
465
 
        ancestor_id = _mod_revision.NULL_REVISION
466
 
    else:
467
 
        ancestor_id = rev.parent_ids[0]
468
 
    tree_1 = repo.revision_tree(ancestor_id)
469
 
    tree_2 = repo.revision_tree(rev.revision_id)
470
 
    if diff_type == 'partial' and files is not None:
471
 
        specific_files = files
472
 
    else:
473
 
        specific_files = None
474
 
    s = BytesIO()
475
 
    path_encoding = get_diff_header_encoding()
476
 
    diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
477
 
                         new_label='', path_encoding=path_encoding)
478
 
    return s.getvalue()
 
420
        return _DefaultLogGenerator(branch, rqst)
479
421
 
480
422
 
481
423
class _StartNotLinearAncestor(Exception):
485
427
class _DefaultLogGenerator(LogGenerator):
486
428
    """The default generator of log revisions."""
487
429
 
488
 
    def __init__(
489
 
            self, branch, levels=None, limit=None, diff_type=None,
490
 
            delta_type=None, show_signature=None, omit_merges=None,
491
 
            generate_tags=None, specific_files=None, match=None,
492
 
            start_revision=None, end_revision=None, direction=None,
493
 
            exclude_common_ancestry=None, _match_using_deltas=None,
494
 
            signature=None):
 
430
    def __init__(self, branch, rqst):
495
431
        self.branch = branch
496
 
        self.levels = levels
497
 
        self.limit = limit
498
 
        self.diff_type = diff_type
499
 
        self.delta_type = delta_type
500
 
        self.show_signature = signature
501
 
        self.omit_merges = omit_merges
502
 
        self.specific_files = specific_files
503
 
        self.match = match
504
 
        self.start_revision = start_revision
505
 
        self.end_revision = end_revision
506
 
        self.direction = direction
507
 
        self.exclude_common_ancestry = exclude_common_ancestry
508
 
        self._match_using_deltas = _match_using_deltas
509
 
        if generate_tags and branch.supports_tags():
 
432
        self.rqst = rqst
 
433
        if rqst.get('generate_tags') and branch.supports_tags():
510
434
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
511
435
        else:
512
436
            self.rev_tag_dict = {}
516
440
 
517
441
        :return: An iterator yielding LogRevision objects.
518
442
        """
 
443
        rqst = self.rqst
 
444
        levels = rqst.get('levels')
 
445
        limit = rqst.get('limit')
 
446
        diff_type = rqst.get('diff_type')
 
447
        show_signature = rqst.get('signature')
 
448
        omit_merges = rqst.get('omit_merges')
519
449
        log_count = 0
520
450
        revision_iterator = self._create_log_revision_iterator()
521
451
        for revs in revision_iterator:
522
452
            for (rev_id, revno, merge_depth), rev, delta in revs:
523
453
                # 0 levels means show everything; merge_depth counts from 0
524
 
                if (self.levels != 0 and merge_depth is not None and
525
 
                        merge_depth >= self.levels):
526
 
                    continue
527
 
                if self.omit_merges and len(rev.parent_ids) > 1:
528
 
                    continue
529
 
                if rev is None:
530
 
                    raise errors.GhostRevisionUnusableHere(rev_id)
531
 
                if self.diff_type is None:
 
454
                if levels != 0 and merge_depth >= levels:
 
455
                    continue
 
456
                if omit_merges and len(rev.parent_ids) > 1:
 
457
                    continue
 
458
                if diff_type is None:
532
459
                    diff = None
533
460
                else:
534
 
                    diff = _format_diff(
535
 
                        self.branch, rev, self.diff_type,
536
 
                        self.specific_files)
537
 
                if self.show_signature:
538
 
                    signature = format_signature_validity(rev_id, self.branch)
 
461
                    diff = self._format_diff(rev, rev_id, diff_type)
 
462
                if show_signature:
 
463
                    signature = format_signature_validity(rev_id,
 
464
                                                self.branch.repository)
539
465
                else:
540
466
                    signature = None
541
 
                yield LogRevision(
542
 
                    rev, revno, merge_depth, delta,
 
467
                yield LogRevision(rev, revno, merge_depth, delta,
543
468
                    self.rev_tag_dict.get(rev_id), diff, signature)
544
 
                if self.limit:
 
469
                if limit:
545
470
                    log_count += 1
546
 
                    if log_count >= self.limit:
 
471
                    if log_count >= limit:
547
472
                        return
548
473
 
 
474
    def _format_diff(self, rev, rev_id, diff_type):
 
475
        repo = self.branch.repository
 
476
        if len(rev.parent_ids) == 0:
 
477
            ancestor_id = _mod_revision.NULL_REVISION
 
478
        else:
 
479
            ancestor_id = rev.parent_ids[0]
 
480
        tree_1 = repo.revision_tree(ancestor_id)
 
481
        tree_2 = repo.revision_tree(rev_id)
 
482
        file_ids = self.rqst.get('specific_fileids')
 
483
        if diff_type == 'partial' and file_ids is not None:
 
484
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
485
        else:
 
486
            specific_files = None
 
487
        s = StringIO()
 
488
        path_encoding = get_diff_header_encoding()
 
489
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
490
            new_label='', path_encoding=path_encoding)
 
491
        return s.getvalue()
 
492
 
549
493
    def _create_log_revision_iterator(self):
550
494
        """Create a revision iterator for log.
551
495
 
552
496
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
553
497
            delta).
554
498
        """
555
 
        start_rev_id, end_rev_id = _get_revision_limits(
556
 
            self.branch, self.start_revision, self.end_revision)
557
 
        if self._match_using_deltas:
558
 
            return _log_revision_iterator_using_delta_matching(
559
 
                self.branch,
560
 
                delta_type=self.delta_type,
561
 
                match=self.match,
562
 
                levels=self.levels,
563
 
                specific_files=self.specific_files,
564
 
                start_rev_id=start_rev_id, end_rev_id=end_rev_id,
565
 
                direction=self.direction,
566
 
                exclude_common_ancestry=self.exclude_common_ancestry,
567
 
                limit=self.limit)
 
499
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
500
            self.branch, self.rqst.get('start_revision'),
 
501
            self.rqst.get('end_revision'))
 
502
        if self.rqst.get('_match_using_deltas'):
 
503
            return self._log_revision_iterator_using_delta_matching()
568
504
        else:
569
505
            # We're using the per-file-graph algorithm. This scales really
570
506
            # well but only makes sense if there is a single file and it's
571
507
            # not a directory
572
 
            file_count = len(self.specific_files)
 
508
            file_count = len(self.rqst.get('specific_fileids'))
573
509
            if file_count != 1:
574
 
                raise errors.BzrError(
575
 
                    "illegal LogRequest: must match-using-deltas "
 
510
                raise BzrError("illegal LogRequest: must match-using-deltas "
576
511
                    "when logging %d files" % file_count)
577
 
            return _log_revision_iterator_using_per_file_graph(
578
 
                self.branch,
579
 
                delta_type=self.delta_type,
580
 
                match=self.match,
581
 
                levels=self.levels,
582
 
                path=self.specific_files[0],
583
 
                start_rev_id=start_rev_id, end_rev_id=end_rev_id,
584
 
                direction=self.direction,
585
 
                exclude_common_ancestry=self.exclude_common_ancestry
586
 
                )
 
512
            return self._log_revision_iterator_using_per_file_graph()
 
513
 
 
514
    def _log_revision_iterator_using_delta_matching(self):
 
515
        # Get the base revisions, filtering by the revision range
 
516
        rqst = self.rqst
 
517
        generate_merge_revisions = rqst.get('levels') != 1
 
518
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
519
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
520
        view_revisions = _calc_view_revisions(
 
521
            self.branch, self.start_rev_id, self.end_rev_id,
 
522
            rqst.get('direction'),
 
523
            generate_merge_revisions=generate_merge_revisions,
 
524
            delayed_graph_generation=delayed_graph_generation,
 
525
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
526
 
 
527
        # Apply the other filters
 
528
        return make_log_rev_iterator(self.branch, view_revisions,
 
529
            rqst.get('delta_type'), rqst.get('match'),
 
530
            file_ids=rqst.get('specific_fileids'),
 
531
            direction=rqst.get('direction'))
 
532
 
 
533
    def _log_revision_iterator_using_per_file_graph(self):
 
534
        # Get the base revisions, filtering by the revision range.
 
535
        # Note that we always generate the merge revisions because
 
536
        # filter_revisions_touching_file_id() requires them ...
 
537
        rqst = self.rqst
 
538
        view_revisions = _calc_view_revisions(
 
539
            self.branch, self.start_rev_id, self.end_rev_id,
 
540
            rqst.get('direction'), generate_merge_revisions=True,
 
541
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
542
        if not isinstance(view_revisions, list):
 
543
            view_revisions = list(view_revisions)
 
544
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
545
            rqst.get('specific_fileids')[0], view_revisions,
 
546
            include_merges=rqst.get('levels') != 1)
 
547
        return make_log_rev_iterator(self.branch, view_revisions,
 
548
            rqst.get('delta_type'), rqst.get('match'))
587
549
 
588
550
 
589
551
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
597
559
             a list of the same tuples.
598
560
    """
599
561
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
 
        raise errors.CommandError(gettext(
 
562
        raise errors.BzrCommandError(gettext(
601
563
            '--exclude-common-ancestry requires two different revisions'))
602
564
    if direction not in ('reverse', 'forward'):
603
565
        raise ValueError(gettext('invalid direction %r') % direction)
604
 
    br_rev_id = branch.last_revision()
605
 
    if br_rev_id == _mod_revision.NULL_REVISION:
 
566
    br_revno, br_rev_id = branch.last_revision_info()
 
567
    if br_revno == 0:
606
568
        return []
607
569
 
608
570
    if (end_rev_id and start_rev_id == end_rev_id
609
571
        and (not generate_merge_revisions
610
572
             or not _has_merges(branch, end_rev_id))):
611
573
        # If a single revision is requested, check we can handle it
612
 
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
613
 
                                      branch.revno())
 
574
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
575
                                       br_revno)
614
576
    if not generate_merge_revisions:
615
577
        try:
616
578
            # If we only want to see linear revisions, we can iterate ...
621
583
            # ancestor of the end limit, check it before outputting anything
622
584
            if (direction == 'forward'
623
585
                or (start_rev_id and not _is_obvious_ancestor(
624
 
                    branch, start_rev_id, end_rev_id))):
625
 
                iter_revs = list(iter_revs)
 
586
                        branch, start_rev_id, end_rev_id))):
 
587
                    iter_revs = list(iter_revs)
626
588
            if direction == 'forward':
627
589
                iter_revs = reversed(iter_revs)
628
590
            return iter_revs
660
622
    initial_revisions = []
661
623
    if delayed_graph_generation:
662
624
        try:
663
 
            for rev_id, revno, depth in _linear_view_revisions(
664
 
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
625
            for rev_id, revno, depth in  _linear_view_revisions(
 
626
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
665
627
                if _has_merges(branch, rev_id):
666
628
                    # The end_rev_id can be nested down somewhere. We need an
667
629
                    # explicit ancestry check. There is an ambiguity here as we
674
636
                    # -- vila 20100319
675
637
                    graph = branch.repository.get_graph()
676
638
                    if (start_rev_id is not None
677
 
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
639
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
678
640
                        raise _StartNotLinearAncestor()
679
641
                    # Since we collected the revisions so far, we need to
680
642
                    # adjust end_rev_id.
688
650
        except _StartNotLinearAncestor:
689
651
            # A merge was never detected so the lower revision limit can't
690
652
            # be nested down somewhere
691
 
            raise errors.CommandError(gettext('Start revision not found in'
692
 
                                                 ' history of end revision.'))
 
653
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
654
                ' history of end revision.'))
693
655
 
694
656
    # We exit the loop above because we encounter a revision with merges, from
695
657
    # this revision, we need to switch to _graph_view_revisions.
699
661
    # shown naturally, i.e. just like it is for linear logging. We can easily
700
662
    # make forward the exact opposite display, but showing the merge revisions
701
663
    # indented at the end seems slightly nicer in that case.
702
 
    view_revisions = itertools.chain(iter(initial_revisions),
703
 
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
704
 
                                                           rebase_initial_depths=(
705
 
                                                               direction == 'reverse'),
706
 
                                                           exclude_common_ancestry=exclude_common_ancestry))
 
664
    view_revisions = chain(iter(initial_revisions),
 
665
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
666
                              rebase_initial_depths=(direction == 'reverse'),
 
667
                              exclude_common_ancestry=exclude_common_ancestry))
707
668
    return view_revisions
708
669
 
709
670
 
741
702
            # both on mainline
742
703
            return start_dotted[0] <= end_dotted[0]
743
704
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
744
 
              start_dotted[0:1] == end_dotted[0:1]):
 
705
            start_dotted[0:1] == end_dotted[0:1]):
745
706
            # both on same development line
746
707
            return start_dotted[2] <= end_dotted[2]
747
708
        else:
761
722
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
762
723
        the iterated revisions.
763
724
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
764
 
        dotted_revno will be None for ghosts
765
725
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
766
726
        is not found walking the left-hand history
767
727
    """
 
728
    br_revno, br_rev_id = branch.last_revision_info()
768
729
    repo = branch.repository
769
730
    graph = repo.get_graph()
770
731
    if start_rev_id is None and end_rev_id is None:
771
 
        if branch._format.stores_revno() or \
772
 
                config.GlobalStack().get('calculate_revnos'):
773
 
            try:
774
 
                br_revno, br_rev_id = branch.last_revision_info()
775
 
            except errors.GhostRevisionsHaveNoRevno:
776
 
                br_rev_id = branch.last_revision()
777
 
                cur_revno = None
778
 
            else:
779
 
                cur_revno = br_revno
780
 
        else:
781
 
            br_rev_id = branch.last_revision()
782
 
            cur_revno = None
783
 
 
784
 
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
 
                                                  (_mod_revision.NULL_REVISION,))
786
 
        while True:
787
 
            try:
788
 
                revision_id = next(graph_iter)
789
 
            except errors.RevisionNotPresent as e:
790
 
                # Oops, a ghost.
791
 
                yield e.revision_id, None, None
792
 
                break
793
 
            except StopIteration:
794
 
                break
795
 
            else:
796
 
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
797
 
                if cur_revno is not None:
798
 
                    cur_revno -= 1
 
732
        cur_revno = br_revno
 
733
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
734
            (_mod_revision.NULL_REVISION,)):
 
735
            yield revision_id, str(cur_revno), 0
 
736
            cur_revno -= 1
799
737
    else:
800
 
        br_rev_id = branch.last_revision()
801
738
        if end_rev_id is None:
802
739
            end_rev_id = br_rev_id
803
740
        found_start = start_rev_id is None
804
 
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
805
 
                                                  (_mod_revision.NULL_REVISION,))
806
 
        while True:
807
 
            try:
808
 
                revision_id = next(graph_iter)
809
 
            except StopIteration:
810
 
                break
811
 
            except errors.RevisionNotPresent as e:
812
 
                # Oops, a ghost.
813
 
                yield e.revision_id, None, None
814
 
                break
815
 
            else:
816
 
                revno_str = _compute_revno_str(branch, revision_id)
817
 
                if not found_start and revision_id == start_rev_id:
818
 
                    if not exclude_common_ancestry:
819
 
                        yield revision_id, revno_str, 0
820
 
                    found_start = True
821
 
                    break
822
 
                else:
 
741
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
742
                (_mod_revision.NULL_REVISION,)):
 
743
            revno_str = _compute_revno_str(branch, revision_id)
 
744
            if not found_start and revision_id == start_rev_id:
 
745
                if not exclude_common_ancestry:
823
746
                    yield revision_id, revno_str, 0
824
 
        if not found_start:
825
 
            raise _StartNotLinearAncestor()
 
747
                found_start = True
 
748
                break
 
749
            else:
 
750
                yield revision_id, revno_str, 0
 
751
        else:
 
752
            if not found_start:
 
753
                raise _StartNotLinearAncestor()
826
754
 
827
755
 
828
756
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
873
801
    """Adjust depths upwards so the top level is 0."""
874
802
    # If either the first or last revision have a merge_depth of 0, we're done
875
803
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
876
 
        min_depth = min([d for r, n, d in view_revisions])
 
804
        min_depth = min([d for r,n,d in view_revisions])
877
805
        if min_depth != 0:
878
 
            view_revisions = [(r, n, d - min_depth)
879
 
                              for r, n, d in view_revisions]
 
806
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
880
807
    return view_revisions
881
808
 
882
809
 
883
810
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
884
 
                          files=None, direction='reverse'):
 
811
        file_ids=None, direction='reverse'):
885
812
    """Create a revision iterator for log.
886
813
 
887
814
    :param branch: The branch being logged.
889
816
    :param generate_delta: Whether to generate a delta for each revision.
890
817
      Permitted values are None, 'full' and 'partial'.
891
818
    :param search: A user text search string.
892
 
    :param files: If non empty, only revisions matching one or more of
893
 
      the files are to be kept.
 
819
    :param file_ids: If non empty, only revisions matching one or more of
 
820
      the file-ids are to be kept.
894
821
    :param direction: the direction in which view_revisions is sorted
895
822
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
896
823
        delta).
897
824
    """
898
825
    # Convert view_revisions into (view, None, None) groups to fit with
899
826
    # the standard interface here.
900
 
    if isinstance(view_revisions, list):
 
827
    if type(view_revisions) == list:
901
828
        # A single batch conversion is faster than many incremental ones.
902
829
        # As we have all the data, do a batch conversion.
903
830
        nones = [None] * len(view_revisions)
904
 
        log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
 
831
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
905
832
    else:
906
833
        def _convert():
907
834
            for view in view_revisions:
911
838
        # It would be nicer if log adapters were first class objects
912
839
        # with custom parameters. This will do for now. IGC 20090127
913
840
        if adapter == _make_delta_filter:
914
 
            log_rev_iterator = adapter(
915
 
                branch, generate_delta, search, log_rev_iterator, files,
916
 
                direction)
 
841
            log_rev_iterator = adapter(branch, generate_delta,
 
842
                search, log_rev_iterator, file_ids, direction)
917
843
        else:
918
 
            log_rev_iterator = adapter(
919
 
                branch, generate_delta, search, log_rev_iterator)
 
844
            log_rev_iterator = adapter(branch, generate_delta,
 
845
                search, log_rev_iterator)
920
846
    return log_rev_iterator
921
847
 
922
848
 
934
860
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
935
861
        delta).
936
862
    """
937
 
    if not match:
 
863
    if match is None:
938
864
        return log_rev_iterator
939
 
    # Use lazy_compile so mapping to InvalidPattern error occurs.
940
 
    searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
941
 
                for k, v in match.items()]
 
865
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
866
                for (k,v) in match.iteritems()]
942
867
    return _filter_re(searchRE, log_rev_iterator)
943
868
 
944
869
 
948
873
        if new_revs:
949
874
            yield new_revs
950
875
 
951
 
 
952
876
def _match_filter(searchRE, rev):
953
877
    strings = {
954
 
        'message': (rev.message,),
955
 
        'committer': (rev.committer,),
956
 
        'author': (rev.get_apparent_authors()),
957
 
        'bugs': list(rev.iter_bugs())
958
 
        }
959
 
    strings[''] = [item for inner_list in strings.values()
 
878
               'message': (rev.message,),
 
879
               'committer': (rev.committer,),
 
880
               'author': (rev.get_apparent_authors()),
 
881
               'bugs': list(rev.iter_bugs())
 
882
               }
 
883
    strings[''] = [item for inner_list in strings.itervalues()
960
884
                   for item in inner_list]
961
 
    for k, v in searchRE:
 
885
    for (k,v) in searchRE:
962
886
        if k in strings and not _match_any_filter(strings[k], v):
963
887
            return False
964
888
    return True
965
889
 
966
 
 
967
890
def _match_any_filter(strings, res):
968
 
    return any(r.search(s) for r in res for s in strings)
969
 
 
 
891
    return any([filter(None, map(re.search, strings)) for re in res])
970
892
 
971
893
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
 
                       files=None, direction='reverse'):
 
894
    fileids=None, direction='reverse'):
973
895
    """Add revision deltas to a log iterator if needed.
974
896
 
975
897
    :param branch: The branch being logged.
978
900
    :param search: A user text search string.
979
901
    :param log_rev_iterator: An input iterator containing all revisions that
980
902
        could be displayed, in lists.
981
 
    :param files: If non empty, only revisions matching one or more of
982
 
      the files are to be kept.
 
903
    :param fileids: If non empty, only revisions matching one or more of
 
904
      the file-ids are to be kept.
983
905
    :param direction: the direction in which view_revisions is sorted
984
906
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
985
907
        delta).
986
908
    """
987
 
    if not generate_delta and not files:
 
909
    if not generate_delta and not fileids:
988
910
        return log_rev_iterator
989
911
    return _generate_deltas(branch.repository, log_rev_iterator,
990
 
                            generate_delta, files, direction)
991
 
 
992
 
 
993
 
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
994
 
                     direction):
 
912
        generate_delta, fileids, direction)
 
913
 
 
914
 
 
915
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
 
916
    direction):
995
917
    """Create deltas for each batch of revisions in log_rev_iterator.
996
918
 
997
919
    If we're only generating deltas for the sake of filtering against
998
 
    files, we stop generating deltas once all files reach the
 
920
    file-ids, we stop generating deltas once all file-ids reach the
999
921
    appropriate life-cycle point. If we're receiving data newest to
1000
922
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1001
923
    """
1002
 
    check_files = files is not None and len(files) > 0
1003
 
    if check_files:
1004
 
        file_set = set(files)
 
924
    check_fileids = fileids is not None and len(fileids) > 0
 
925
    if check_fileids:
 
926
        fileid_set = set(fileids)
1005
927
        if direction == 'reverse':
1006
928
            stop_on = 'add'
1007
929
        else:
1008
930
            stop_on = 'remove'
1009
931
    else:
1010
 
        file_set = None
 
932
        fileid_set = None
1011
933
    for revs in log_rev_iterator:
1012
 
        # If we were matching against files and we've run out,
 
934
        # If we were matching against fileids and we've run out,
1013
935
        # there's nothing left to do
1014
 
        if check_files and not file_set:
 
936
        if check_fileids and not fileid_set:
1015
937
            return
1016
938
        revisions = [rev[1] for rev in revs]
1017
939
        new_revs = []
1018
 
        if delta_type == 'full' and not check_files:
1019
 
            deltas = repository.get_revision_deltas(revisions)
1020
 
            for rev, delta in zip(revs, deltas):
 
940
        if delta_type == 'full' and not check_fileids:
 
941
            deltas = repository.get_deltas_for_revisions(revisions)
 
942
            for rev, delta in izip(revs, deltas):
1021
943
                new_revs.append((rev[0], rev[1], delta))
1022
944
        else:
1023
 
            deltas = repository.get_revision_deltas(
1024
 
                revisions, specific_files=file_set)
1025
 
            for rev, delta in zip(revs, deltas):
1026
 
                if check_files:
 
945
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
 
946
            for rev, delta in izip(revs, deltas):
 
947
                if check_fileids:
1027
948
                    if delta is None or not delta.has_changed():
1028
949
                        continue
1029
950
                    else:
1030
 
                        _update_files(delta, file_set, stop_on)
 
951
                        _update_fileids(delta, fileid_set, stop_on)
1031
952
                        if delta_type is None:
1032
953
                            delta = None
1033
954
                        elif delta_type == 'full':
1044
965
        yield new_revs
1045
966
 
1046
967
 
1047
 
def _update_files(delta, files, stop_on):
1048
 
    """Update the set of files to search based on file lifecycle events.
 
968
def _update_fileids(delta, fileids, stop_on):
 
969
    """Update the set of file-ids to search based on file lifecycle events.
1049
970
 
1050
 
    :param files: a set of files to update
1051
 
    :param stop_on: either 'add' or 'remove' - take files out of the
1052
 
      files set once their add or remove entry is detected respectively
 
971
    :param fileids: a set of fileids to update
 
972
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
973
      fileids set once their add or remove entry is detected respectively
1053
974
    """
1054
975
    if stop_on == 'add':
1055
976
        for item in delta.added:
1056
 
            if item.path[1] in files:
1057
 
                files.remove(item.path[1])
1058
 
        for item in delta.copied + delta.renamed:
1059
 
            if item.path[1] in files:
1060
 
                files.remove(item.path[1])
1061
 
                files.add(item.path[0])
1062
 
            if item.kind[1] == 'directory':
1063
 
                for path in list(files):
1064
 
                    if is_inside(item.path[1], path):
1065
 
                        files.remove(path)
1066
 
                        files.add(item.path[0] + path[len(item.path[1]):])
 
977
            if item[1] in fileids:
 
978
                fileids.remove(item[1])
1067
979
    elif stop_on == 'delete':
1068
980
        for item in delta.removed:
1069
 
            if item.path[0] in files:
1070
 
                files.remove(item.path[0])
1071
 
        for item in delta.copied + delta.renamed:
1072
 
            if item.path[0] in files:
1073
 
                files.remove(item.path[0])
1074
 
                files.add(item.path[1])
1075
 
            if item.kind[0] == 'directory':
1076
 
                for path in list(files):
1077
 
                    if is_inside(item.path[0], path):
1078
 
                        files.remove(path)
1079
 
                        files.add(item.path[1] + path[len(item.path[0]):])
 
981
            if item[1] in fileids:
 
982
                fileids.remove(item[1])
1080
983
 
1081
984
 
1082
985
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1094
997
    for revs in log_rev_iterator:
1095
998
        # r = revision_id, n = revno, d = merge depth
1096
999
        revision_ids = [view[0] for view, _, _ in revs]
1097
 
        revisions = dict(repository.iter_revisions(revision_ids))
1098
 
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
 
1000
        revisions = repository.get_revisions(revision_ids)
 
1001
        revs = [(rev[0], revision, rev[2]) for rev, revision in
 
1002
            izip(revs, revisions)]
 
1003
        yield revs
1099
1004
 
1100
1005
 
1101
1006
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
1123
1028
def _get_revision_limits(branch, start_revision, end_revision):
1124
1029
    """Get and check revision limits.
1125
1030
 
1126
 
    :param branch: The branch containing the revisions.
1127
 
 
1128
 
    :param start_revision: The first revision to be logged, as a RevisionInfo.
1129
 
 
1130
 
    :param end_revision: The last revision to be logged, as a RevisionInfo
 
1031
    :param  branch: The branch containing the revisions.
 
1032
 
 
1033
    :param  start_revision: The first revision to be logged.
 
1034
            For backwards compatibility this may be a mainline integer revno,
 
1035
            but for merge revision support a RevisionInfo is expected.
 
1036
 
 
1037
    :param  end_revision: The last revision to be logged.
 
1038
            For backwards compatibility this may be a mainline integer revno,
 
1039
            but for merge revision support a RevisionInfo is expected.
1131
1040
 
1132
1041
    :return: (start_rev_id, end_rev_id) tuple.
1133
1042
    """
 
1043
    branch_revno, branch_rev_id = branch.last_revision_info()
1134
1044
    start_rev_id = None
1135
 
    start_revno = None
1136
 
    if start_revision is not None:
1137
 
        if not isinstance(start_revision, revisionspec.RevisionInfo):
1138
 
            raise TypeError(start_revision)
1139
 
        start_rev_id = start_revision.rev_id
1140
 
        start_revno = start_revision.revno
1141
 
    if start_revno is None:
 
1045
    if start_revision is None:
1142
1046
        start_revno = 1
 
1047
    else:
 
1048
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
1049
            start_rev_id = start_revision.rev_id
 
1050
            start_revno = start_revision.revno or 1
 
1051
        else:
 
1052
            branch.check_real_revno(start_revision)
 
1053
            start_revno = start_revision
 
1054
            start_rev_id = branch.get_rev_id(start_revno)
1143
1055
 
1144
1056
    end_rev_id = None
1145
 
    end_revno = None
1146
 
    if end_revision is not None:
1147
 
        if not isinstance(end_revision, revisionspec.RevisionInfo):
1148
 
            raise TypeError(start_revision)
1149
 
        end_rev_id = end_revision.rev_id
1150
 
        end_revno = end_revision.revno
 
1057
    if end_revision is None:
 
1058
        end_revno = branch_revno
 
1059
    else:
 
1060
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
1061
            end_rev_id = end_revision.rev_id
 
1062
            end_revno = end_revision.revno or branch_revno
 
1063
        else:
 
1064
            branch.check_real_revno(end_revision)
 
1065
            end_revno = end_revision
 
1066
            end_rev_id = branch.get_rev_id(end_revno)
1151
1067
 
1152
 
    if branch.last_revision() != _mod_revision.NULL_REVISION:
 
1068
    if branch_revno != 0:
1153
1069
        if (start_rev_id == _mod_revision.NULL_REVISION
1154
 
                or end_rev_id == _mod_revision.NULL_REVISION):
1155
 
            raise errors.CommandError(
1156
 
                gettext('Logging revision 0 is invalid.'))
1157
 
        if end_revno is not None and start_revno > end_revno:
1158
 
            raise errors.CommandError(
1159
 
                gettext("Start revision must be older than the end revision."))
 
1070
            or end_rev_id == _mod_revision.NULL_REVISION):
 
1071
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1072
        if start_revno > end_revno:
 
1073
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1074
                                         "older than the end revision."))
1160
1075
    return (start_rev_id, end_rev_id)
1161
1076
 
1162
1077
 
1210
1125
            end_revno = end_revision
1211
1126
 
1212
1127
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1213
 
            or (end_rev_id == _mod_revision.NULL_REVISION)):
1214
 
        raise errors.CommandError(gettext('Logging revision 0 is invalid.'))
 
1128
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1129
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1215
1130
    if start_revno > end_revno:
1216
 
        raise errors.CommandError(gettext("Start revision must be older "
1217
 
                                             "than the end revision."))
 
1131
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1132
                                     "than the end revision."))
1218
1133
 
1219
1134
    if end_revno < start_revno:
1220
1135
        return None, None, None, None
1243
1158
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1244
1159
 
1245
1160
 
1246
 
def _filter_revisions_touching_path(branch, path, view_revisions,
1247
 
                                    include_merges=True):
1248
 
    r"""Return the list of revision ids which touch a given path.
 
1161
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1162
    include_merges=True):
 
1163
    r"""Return the list of revision ids which touch a given file id.
1249
1164
 
1250
1165
    The function filters view_revisions and returns a subset.
1251
 
    This includes the revisions which directly change the path,
 
1166
    This includes the revisions which directly change the file id,
1252
1167
    and the revisions which merge these changes. So if the
1253
1168
    revision graph is::
1254
1169
 
1271
1186
 
1272
1187
    :param branch: The branch where we can get text revision information.
1273
1188
 
1274
 
    :param path: Filter out revisions that do not touch path.
 
1189
    :param file_id: Filter out revisions that do not touch file_id.
1275
1190
 
1276
1191
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1277
1192
        tuples. This is the list of revisions which will be filtered. It is
1285
1200
    # Lookup all possible text keys to determine which ones actually modified
1286
1201
    # the file.
1287
1202
    graph = branch.repository.get_file_graph()
1288
 
    start_tree = branch.repository.revision_tree(view_revisions[0][0])
1289
 
    file_id = start_tree.path2id(path)
1290
1203
    get_parent_map = graph.get_parent_map
1291
1204
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1292
1205
    next_keys = None
1299
1212
    #       rate). This particular access is clustered with a low success rate.
1300
1213
    modified_text_revisions = set()
1301
1214
    chunk_size = 1000
1302
 
    for start in range(0, len(text_keys), chunk_size):
 
1215
    for start in xrange(0, len(text_keys), chunk_size):
1303
1216
        next_keys = text_keys[start:start + chunk_size]
1304
1217
        # Only keep the revision_id portion of the key
1305
1218
        modified_text_revisions.update(
1320
1233
 
1321
1234
        if rev_id in modified_text_revisions:
1322
1235
            # This needs to be logged, along with the extra revisions
1323
 
            for idx in range(len(current_merge_stack)):
 
1236
            for idx in xrange(len(current_merge_stack)):
1324
1237
                node = current_merge_stack[idx]
1325
1238
                if node is not None:
1326
1239
                    if include_merges or node[2] == 0:
1333
1246
    """Reverse revisions by depth.
1334
1247
 
1335
1248
    Revisions with a different depth are sorted as a group with the previous
1336
 
    revision of that depth.  There may be no topological justification for this
 
1249
    revision of that depth.  There may be no topological justification for this,
1337
1250
    but it looks much nicer.
1338
1251
    """
1339
1252
    # Add a fake revision at start so that we can always attach sub revisions
1446
1359
        """
1447
1360
        self.to_file = to_file
1448
1361
        # 'exact' stream used to show diff, it should print content 'as is'
1449
 
        # and should not try to decode/encode it to unicode to avoid bug
1450
 
        # #328007
 
1362
        # and should not try to decode/encode it to unicode to avoid bug #328007
1451
1363
        if to_exact_file is not None:
1452
1364
            self.to_exact_file = to_exact_file
1453
1365
        else:
1454
 
            # XXX: somewhat hacky; this assumes it's a codec writer; it's
1455
 
            # better for code that expects to get diffs to pass in the exact
1456
 
            # file stream
 
1366
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1367
            # for code that expects to get diffs to pass in the exact file
 
1368
            # stream
1457
1369
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1458
1370
        self.show_ids = show_ids
1459
1371
        self.show_timezone = show_timezone
1460
1372
        if delta_format is None:
1461
1373
            # Ensures backward compatibility
1462
 
            delta_format = 2  # long format
 
1374
            delta_format = 2 # long format
1463
1375
        self.delta_format = delta_format
1464
1376
        self.levels = levels
1465
1377
        self._show_advice = show_advice
1563
1475
        """
1564
1476
        lines = self._foreign_info_properties(revision)
1565
1477
        for key, handler in properties_handler_registry.iteritems():
1566
 
            try:
1567
 
                lines.extend(self._format_properties(handler(revision)))
1568
 
            except Exception:
1569
 
                trace.log_exception_quietly()
1570
 
                trace.print_exception(sys.exc_info(), self.to_file)
 
1478
            lines.extend(self._format_properties(handler(revision)))
1571
1479
        return lines
1572
1480
 
1573
1481
    def _foreign_info_properties(self, rev):
1581
1489
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1582
1490
 
1583
1491
        # Imported foreign revision revision ids always contain :
1584
 
        if b":" not in rev.revision_id:
 
1492
        if not ":" in rev.revision_id:
1585
1493
            return []
1586
1494
 
1587
1495
        # Revision was once imported from a foreign repository
1601
1509
        return lines
1602
1510
 
1603
1511
    def show_diff(self, to_file, diff, indent):
1604
 
        encoding = get_terminal_encoding()
1605
 
        for l in diff.rstrip().split(b'\n'):
1606
 
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
 
1512
        for l in diff.rstrip().split('\n'):
 
1513
            to_file.write(indent + '%s\n' % (l,))
1607
1514
 
1608
1515
 
1609
1516
# Separator between revisions in long format
1632
1539
 
1633
1540
    def _date_string_original_timezone(self, rev):
1634
1541
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1635
 
                                                            rev.timezone or 0)
 
1542
            rev.timezone or 0)
1636
1543
 
1637
1544
    def log_revision(self, revision):
1638
1545
        """Log a revision, either merged or not."""
1640
1547
        lines = [_LONG_SEP]
1641
1548
        if revision.revno is not None:
1642
1549
            lines.append('revno: %s%s' % (revision.revno,
1643
 
                                          self.merge_marker(revision)))
 
1550
                self.merge_marker(revision)))
1644
1551
        if revision.tags:
1645
 
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1552
            lines.append('tags: %s' % (', '.join(revision.tags)))
1646
1553
        if self.show_ids or revision.revno is None:
1647
 
            lines.append('revision-id: %s' %
1648
 
                         (revision.rev.revision_id.decode('utf-8'),))
 
1554
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1649
1555
        if self.show_ids:
1650
1556
            for parent_id in revision.rev.parent_ids:
1651
 
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1557
                lines.append('parent: %s' % (parent_id,))
1652
1558
        lines.extend(self.custom_properties(revision.rev))
1653
1559
 
1654
1560
        committer = revision.rev.committer
1679
1585
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
1680
1586
        if revision.delta is not None:
1681
1587
            # Use the standard status output to display changes
1682
 
            from breezy.delta import report_delta
 
1588
            from bzrlib.delta import report_delta
1683
1589
            report_delta(to_file, revision.delta, short_status=False,
1684
1590
                         show_ids=self.show_ids, indent=indent)
1685
1591
        if revision.diff is not None:
1730
1636
        to_file = self.to_file
1731
1637
        tags = ''
1732
1638
        if revision.tags:
1733
 
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1639
            tags = ' {%s}' % (', '.join(revision.tags))
1734
1640
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1735
 
                                                     revision.revno or "", self.short_author(
1736
 
                                                         revision.rev),
1737
 
                                                     format_date(revision.rev.timestamp,
1738
 
                                                                 revision.rev.timezone or 0,
1739
 
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
1740
 
                                                                 show_offset=False),
1741
 
                                                     tags, self.merge_marker(revision)))
1742
 
        self.show_properties(revision.rev, indent + offset)
 
1641
                revision.revno or "", self.short_author(revision.rev),
 
1642
                format_date(revision.rev.timestamp,
 
1643
                            revision.rev.timezone or 0,
 
1644
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
1645
                            show_offset=False),
 
1646
                tags, self.merge_marker(revision)))
 
1647
        self.show_properties(revision.rev, indent+offset)
1743
1648
        if self.show_ids or revision.revno is None:
1744
1649
            to_file.write(indent + offset + 'revision-id:%s\n'
1745
 
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1650
                          % (revision.rev.revision_id,))
1746
1651
        if not revision.rev.message:
1747
1652
            to_file.write(indent + offset + '(no message)\n')
1748
1653
        else:
1752
1657
 
1753
1658
        if revision.delta is not None:
1754
1659
            # Use the standard status output to display changes
1755
 
            from breezy.delta import report_delta
 
1660
            from bzrlib.delta import report_delta
1756
1661
            report_delta(to_file, revision.delta,
1757
 
                         short_status=self.delta_format == 1,
 
1662
                         short_status=self.delta_format==1,
1758
1663
                         show_ids=self.show_ids, indent=indent + offset)
1759
1664
        if revision.diff is not None:
1760
1665
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1778
1683
    def truncate(self, str, max_len):
1779
1684
        if max_len is None or len(str) <= max_len:
1780
1685
            return str
1781
 
        return str[:max_len - 3] + '...'
 
1686
        return str[:max_len-3] + '...'
1782
1687
 
1783
1688
    def date_string(self, rev):
1784
1689
        return format_date(rev.timestamp, rev.timezone or 0,
1794
1699
    def log_revision(self, revision):
1795
1700
        indent = '  ' * revision.merge_depth
1796
1701
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1797
 
                                           self._max_chars, revision.tags, indent))
 
1702
            self._max_chars, revision.tags, indent))
1798
1703
        self.to_file.write('\n')
1799
1704
 
1800
1705
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1813
1718
            # show revno only when is not None
1814
1719
            out.append("%s:" % revno)
1815
1720
        if max_chars is not None:
1816
 
            out.append(self.truncate(
1817
 
                self.short_author(rev), (max_chars + 3) // 4))
 
1721
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1818
1722
        else:
1819
1723
            out.append(self.short_author(rev))
1820
1724
        out.append(self.date_string(rev))
1821
1725
        if len(rev.parent_ids) > 1:
1822
1726
            out.append('[merge]')
1823
1727
        if tags:
1824
 
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1728
            tag_str = '{%s}' % (', '.join(tags))
1825
1729
            out.append(tag_str)
1826
1730
        out.append(rev.get_summary())
1827
1731
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1843
1747
                               show_offset=False)
1844
1748
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1845
1749
        committer_str = committer_str.replace(' <', '  <')
1846
 
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
 
1750
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1847
1751
 
1848
1752
        if revision.delta is not None and revision.delta.has_changed():
1849
1753
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1850
 
                if c.path[0] is None:
1851
 
                    path = c.path[1]
1852
 
                else:
1853
 
                    path = c.path[0]
 
1754
                path, = c[:1]
1854
1755
                to_file.write('\t* %s:\n' % (path,))
1855
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1756
            for c in revision.delta.renamed:
 
1757
                oldpath,newpath = c[:2]
1856
1758
                # For renamed files, show both the old and the new path
1857
 
                to_file.write('\t* %s:\n\t* %s:\n' % (c.path[0], c.path[1]))
 
1759
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
1858
1760
            to_file.write('\n')
1859
1761
 
1860
1762
        if not revision.rev.message:
1913
1815
    try:
1914
1816
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1915
1817
    except KeyError:
1916
 
        raise errors.CommandError(
1917
 
            gettext("unknown log formatter: %r") % name)
 
1818
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1918
1819
 
1919
1820
 
1920
1821
def author_list_all(rev):
1956
1857
    """
1957
1858
    if to_file is None:
1958
1859
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1959
 
                                                            errors='replace')
 
1860
            errors='replace')
1960
1861
    lf = log_formatter(log_format,
1961
1862
                       show_ids=False,
1962
1863
                       to_file=to_file,
1965
1866
    # This is the first index which is different between
1966
1867
    # old and new
1967
1868
    base_idx = None
1968
 
    for i in range(max(len(new_rh), len(old_rh))):
 
1869
    for i in xrange(max(len(new_rh),
 
1870
                        len(old_rh))):
1969
1871
        if (len(new_rh) <= i
1970
1872
            or len(old_rh) <= i
1971
 
                or new_rh[i] != old_rh[i]):
 
1873
            or new_rh[i] != old_rh[i]):
1972
1874
            base_idx = i
1973
1875
            break
1974
1876
 
1975
1877
    if base_idx is None:
1976
1878
        to_file.write('Nothing seems to have changed\n')
1977
1879
        return
1978
 
    # TODO: It might be nice to do something like show_log
1979
 
    # and show the merged entries. But since this is the
1980
 
    # removed revisions, it shouldn't be as important
 
1880
    ## TODO: It might be nice to do something like show_log
 
1881
    ##       and show the merged entries. But since this is the
 
1882
    ##       removed revisions, it shouldn't be as important
1981
1883
    if base_idx < len(old_rh):
1982
 
        to_file.write('*' * 60)
 
1884
        to_file.write('*'*60)
1983
1885
        to_file.write('\nRemoved Revisions:\n')
1984
1886
        for i in range(base_idx, len(old_rh)):
1985
1887
            rev = branch.repository.get_revision(old_rh[i])
1986
 
            lr = LogRevision(rev, i + 1, 0, None)
 
1888
            lr = LogRevision(rev, i+1, 0, None)
1987
1889
            lf.log_revision(lr)
1988
 
        to_file.write('*' * 60)
 
1890
        to_file.write('*'*60)
1989
1891
        to_file.write('\n\n')
1990
1892
    if base_idx < len(new_rh):
1991
1893
        to_file.write('Added Revisions:\n')
1992
1894
        show_log(branch,
1993
1895
                 lf,
 
1896
                 None,
1994
1897
                 verbose=False,
1995
1898
                 direction='forward',
1996
 
                 start_revision=base_idx + 1,
1997
 
                 end_revision=len(new_rh))
 
1899
                 start_revision=base_idx+1,
 
1900
                 end_revision=len(new_rh),
 
1901
                 search=None)
1998
1902
 
1999
1903
 
2000
1904
def get_history_change(old_revision_id, new_revision_id, repository):
2019
1923
    while do_new or do_old:
2020
1924
        if do_new:
2021
1925
            try:
2022
 
                new_revision = next(new_iter)
 
1926
                new_revision = new_iter.next()
2023
1927
            except StopIteration:
2024
1928
                do_new = False
2025
1929
            else:
2030
1934
                    break
2031
1935
        if do_old:
2032
1936
            try:
2033
 
                old_revision = next(old_iter)
 
1937
                old_revision = old_iter.next()
2034
1938
            except StopIteration:
2035
1939
                do_old = False
2036
1940
            else:
2066
1970
    log_format = log_formatter_registry.get_default(branch)
2067
1971
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
2068
1972
    if old_history != []:
2069
 
        output.write('*' * 60)
 
1973
        output.write('*'*60)
2070
1974
        output.write('\nRemoved Revisions:\n')
2071
1975
        show_flat_log(branch.repository, old_history, old_revno, lf)
2072
 
        output.write('*' * 60)
 
1976
        output.write('*'*60)
2073
1977
        output.write('\n\n')
2074
1978
    if new_history != []:
2075
1979
        output.write('Added Revisions:\n')
2076
1980
        start_revno = new_revno - len(new_history) + 1
2077
 
        show_log(branch, lf, verbose=False, direction='forward',
2078
 
                 start_revision=start_revno)
 
1981
        show_log(branch, lf, None, verbose=False, direction='forward',
 
1982
                 start_revision=start_revno,)
2079
1983
 
2080
1984
 
2081
1985
def show_flat_log(repository, history, last_revno, lf):
2086
1990
    :param last_revno: The revno of the last revision_id in the history.
2087
1991
    :param lf: The log formatter to use.
2088
1992
    """
 
1993
    start_revno = last_revno - len(history) + 1
2089
1994
    revisions = repository.get_revisions(history)
2090
1995
    for i, rev in enumerate(revisions):
2091
1996
        lr = LogRevision(rev, i + last_revno, 0, None)
2092
1997
        lf.log_revision(lr)
2093
1998
 
2094
1999
 
2095
 
def _get_info_for_log_files(revisionspec_list, file_list, exit_stack):
2096
 
    """Find files and kinds given a list of files and a revision range.
 
2000
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2001
    """Find file-ids and kinds given a list of files and a revision range.
2097
2002
 
2098
2003
    We search for files at the end of the range. If not found there,
2099
2004
    we try the start of the range.
2102
2007
    :param file_list: the list of paths given on the command line;
2103
2008
      the first of these can be a branch location or a file path,
2104
2009
      the remainder must be file paths
2105
 
    :param exit_stack: When the branch returned is read locked,
2106
 
      an unlock call will be queued to the exit stack.
 
2010
    :param add_cleanup: When the branch returned is read locked,
 
2011
      an unlock call will be queued to the cleanup.
2107
2012
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2108
 
      info_list is a list of (relative_path, found, kind) tuples where
 
2013
      info_list is a list of (relative_path, file_id, kind) tuples where
2109
2014
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2110
2015
      branch will be read-locked.
2111
2016
    """
2112
 
    from breezy.builtins import _get_revision_range
 
2017
    from bzrlib.builtins import _get_revision_range
2113
2018
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2114
2019
        file_list[0])
2115
 
    exit_stack.enter_context(b.lock_read())
 
2020
    add_cleanup(b.lock_read().unlock)
2116
2021
    # XXX: It's damn messy converting a list of paths to relative paths when
2117
2022
    # those paths might be deleted ones, they might be on a case-insensitive
2118
2023
    # filesystem and/or they might be in silly locations (like another branch).
2127
2032
        relpaths = [path] + file_list[1:]
2128
2033
    info_list = []
2129
2034
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2130
 
                                                       "log")
 
2035
        "log")
2131
2036
    if relpaths in ([], [u'']):
2132
2037
        return b, [], start_rev_info, end_rev_info
2133
2038
    if start_rev_info is None and end_rev_info is None:
2135
2040
            tree = b.basis_tree()
2136
2041
        tree1 = None
2137
2042
        for fp in relpaths:
2138
 
            kind = _get_kind_for_file(tree, fp)
2139
 
            if not kind:
 
2043
            file_id = tree.path2id(fp)
 
2044
            kind = _get_kind_for_file_id(tree, file_id)
 
2045
            if file_id is None:
2140
2046
                # go back to when time began
2141
2047
                if tree1 is None:
2142
2048
                    try:
2143
2049
                        rev1 = b.get_rev_id(1)
2144
2050
                    except errors.NoSuchRevision:
2145
2051
                        # No history at all
 
2052
                        file_id = None
2146
2053
                        kind = None
2147
2054
                    else:
2148
2055
                        tree1 = b.repository.revision_tree(rev1)
2149
2056
                if tree1:
2150
 
                    kind = _get_kind_for_file(tree1, fp)
2151
 
            info_list.append((fp, kind))
 
2057
                    file_id = tree1.path2id(fp)
 
2058
                    kind = _get_kind_for_file_id(tree1, file_id)
 
2059
            info_list.append((fp, file_id, kind))
2152
2060
 
2153
2061
    elif start_rev_info == end_rev_info:
2154
2062
        # One revision given - file must exist in it
2155
2063
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2156
2064
        for fp in relpaths:
2157
 
            kind = _get_kind_for_file(tree, fp)
2158
 
            info_list.append((fp, kind))
 
2065
            file_id = tree.path2id(fp)
 
2066
            kind = _get_kind_for_file_id(tree, file_id)
 
2067
            info_list.append((fp, file_id, kind))
2159
2068
 
2160
2069
    else:
2161
2070
        # Revision range given. Get the file-id from the end tree.
2167
2076
            tree = b.repository.revision_tree(rev_id)
2168
2077
        tree1 = None
2169
2078
        for fp in relpaths:
2170
 
            kind = _get_kind_for_file(tree, fp)
2171
 
            if not kind:
 
2079
            file_id = tree.path2id(fp)
 
2080
            kind = _get_kind_for_file_id(tree, file_id)
 
2081
            if file_id is None:
2172
2082
                if tree1 is None:
2173
2083
                    rev_id = start_rev_info.rev_id
2174
2084
                    if rev_id is None:
2176
2086
                        tree1 = b.repository.revision_tree(rev1)
2177
2087
                    else:
2178
2088
                        tree1 = b.repository.revision_tree(rev_id)
2179
 
                kind = _get_kind_for_file(tree1, fp)
2180
 
            info_list.append((fp, kind))
 
2089
                file_id = tree1.path2id(fp)
 
2090
                kind = _get_kind_for_file_id(tree1, file_id)
 
2091
            info_list.append((fp, file_id, kind))
2181
2092
    return b, info_list, start_rev_info, end_rev_info
2182
2093
 
2183
2094
 
2184
 
def _get_kind_for_file(tree, path):
2185
 
    """Return the kind of a path or None if it doesn't exist."""
2186
 
    with tree.lock_read():
2187
 
        try:
2188
 
            return tree.stored_kind(path)
2189
 
        except errors.NoSuchFile:
2190
 
            return None
 
2095
def _get_kind_for_file_id(tree, file_id):
 
2096
    """Return the kind of a file-id or None if it doesn't exist."""
 
2097
    if file_id is not None:
 
2098
        return tree.kind(file_id)
 
2099
    else:
 
2100
        return None
2191
2101
 
2192
2102
 
2193
2103
properties_handler_registry = registry.Registry()
2194
2104
 
2195
2105
# Use the properties handlers to print out bug information if available
2196
 
 
2197
 
 
2198
2106
def _bugs_properties_handler(revision):
2199
 
    fixed_bug_urls = []
2200
 
    related_bug_urls = []
2201
 
    for bug_url, status in revision.iter_bugs():
2202
 
        if status == 'fixed':
2203
 
            fixed_bug_urls.append(bug_url)
2204
 
        elif status == 'related':
2205
 
            related_bug_urls.append(bug_url)
2206
 
    ret = {}
2207
 
    if fixed_bug_urls:
2208
 
        text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
2209
 
        ret[text] = ' '.join(fixed_bug_urls)
2210
 
    if related_bug_urls:
2211
 
        text = ngettext('related bug', 'related bugs',
2212
 
                        len(related_bug_urls))
2213
 
        ret[text] = ' '.join(related_bug_urls)
2214
 
    return ret
 
2107
    if revision.properties.has_key('bugs'):
 
2108
        bug_lines = revision.properties['bugs'].split('\n')
 
2109
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2110
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2111
                          len(row) > 1 and row[1] == 'fixed']
2215
2112
 
 
2113
        if fixed_bug_urls:
 
2114
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2115
                    ' '.join(fixed_bug_urls)}
 
2116
    return {}
2216
2117
 
2217
2118
properties_handler_registry.register('bugs_properties_handler',
2218
2119
                                     _bugs_properties_handler)