/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to breezy/log.py

  • Committer: Jelmer Vernooij
  • Date: 2017-07-23 22:06:41 UTC
  • mfrom: (6738 trunk)
  • mto: This revision was merged to the branch mainline in revision 6739.
  • Revision ID: jelmer@jelmer.uk-20170723220641-69eczax9bmv8d6kk
Merge trunk, address review comments.

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
53
import itertools
53
54
import re
54
55
import sys
63
64
    config,
64
65
    controldir,
65
66
    diff,
 
67
    errors,
66
68
    foreign,
67
 
    lazy_regex,
 
69
    repository as _mod_repository,
68
70
    revision as _mod_revision,
 
71
    revisionspec,
 
72
    tsort,
69
73
    )
70
74
from breezy.i18n import gettext, ngettext
71
75
""")
72
76
 
73
77
from . import (
74
 
    errors,
 
78
    lazy_regex,
75
79
    registry,
76
 
    revisionspec,
77
 
    trace,
78
80
    )
79
81
from .osutils import (
80
82
    format_date,
81
83
    format_date_with_offset_in_original_timezone,
82
84
    get_diff_header_encoding,
83
85
    get_terminal_encoding,
84
 
    is_inside,
85
86
    terminal_width,
86
87
    )
87
 
from .tree import (
88
 
    find_previous_path,
89
 
    InterTree,
 
88
from breezy.sixish import (
 
89
    BytesIO,
 
90
    range,
 
91
    zip,
90
92
    )
91
93
 
92
94
 
93
 
def find_touching_revisions(repository, last_revision, last_tree, last_path):
94
 
    """Yield a description of revisions which affect the file.
 
95
def find_touching_revisions(branch, file_id):
 
96
    """Yield a description of revisions which affect the file_id.
95
97
 
96
98
    Each returned element is (revno, revision_id, description)
97
99
 
101
103
    TODO: Perhaps some way to limit this to only particular revisions,
102
104
    or to traverse a non-mainline set of revisions?
103
105
    """
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)
 
106
    last_ie = None
 
107
    last_path = None
 
108
    revno = 1
 
109
    graph = branch.repository.get_graph()
 
110
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
111
        [_mod_revision.NULL_REVISION]))
 
112
    for revision_id in reversed(history):
 
113
        this_inv = branch.repository.get_inventory(revision_id)
 
114
        if this_inv.has_id(file_id):
 
115
            this_ie = this_inv[file_id]
 
116
            this_path = this_inv.id2path(file_id)
 
117
        else:
 
118
            this_ie = this_path = None
112
119
 
113
120
        # now we know how it was last time, and how it is in this revision.
114
121
        # 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
 
122
 
 
123
        if not this_ie and not last_ie:
 
124
            # not present in either
 
125
            pass
 
126
        elif this_ie and not last_ie:
 
127
            yield revno, revision_id, "added " + this_path
 
128
        elif not this_ie and last_ie:
 
129
            # deleted here
 
130
            yield revno, revision_id, "deleted " + last_path
120
131
        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
 
132
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
 
133
        elif (this_ie.text_size != last_ie.text_size
 
134
              or this_ie.text_sha1 != last_ie.text_sha1):
 
135
            yield revno, revision_id, "modified " + this_path
127
136
 
128
 
        last_verifier = this_verifier
 
137
        last_ie = this_ie
129
138
        last_path = this_path
130
 
        last_tree = this_tree
131
 
        if last_path is None:
132
 
            return
133
 
        revno -= 1
 
139
        revno += 1
134
140
 
135
141
 
136
142
def show_log(branch,
137
143
             lf,
 
144
             specific_fileid=None,
138
145
             verbose=False,
139
146
             direction='reverse',
140
147
             start_revision=None,
141
148
             end_revision=None,
 
149
             search=None,
142
150
             limit=None,
143
151
             show_diff=False,
144
152
             match=None):
151
159
 
152
160
    :param lf: The LogFormatter object showing the output.
153
161
 
 
162
    :param specific_fileid: If not None, list only the commits affecting the
 
163
        specified file, rather than all commits.
 
164
 
154
165
    :param verbose: If True show added/changed/deleted/renamed files.
155
166
 
156
167
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
160
171
 
161
172
    :param end_revision: If not None, only show revisions <= end_revision
162
173
 
 
174
    :param search: If not None, only show revisions with matching commit
 
175
        messages
 
176
 
163
177
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
164
178
        if None or 0.
165
179
 
168
182
    :param match: Dictionary of search lists to use when matching revision
169
183
      properties.
170
184
    """
 
185
    # Convert old-style parameters to new-style parameters
 
186
    if specific_fileid is not None:
 
187
        file_ids = [specific_fileid]
 
188
    else:
 
189
        file_ids = None
171
190
    if verbose:
172
 
        delta_type = 'full'
 
191
        if file_ids:
 
192
            delta_type = 'partial'
 
193
        else:
 
194
            delta_type = 'full'
173
195
    else:
174
196
        delta_type = None
175
197
    if show_diff:
176
 
        diff_type = 'full'
 
198
        if file_ids:
 
199
            diff_type = 'partial'
 
200
        else:
 
201
            diff_type = 'full'
177
202
    else:
178
203
        diff_type = None
179
204
 
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
205
    # Build the request and execute it
196
 
    rqst = make_log_request_dict(
197
 
        direction=direction,
 
206
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
198
207
        start_revision=start_revision, end_revision=end_revision,
199
 
        limit=limit, delta_type=delta_type, diff_type=diff_type)
 
208
        limit=limit, message_search=search,
 
209
        delta_type=delta_type, diff_type=diff_type)
200
210
    Logger(branch, rqst).show(lf)
201
211
 
202
212
 
211
221
    }
212
222
 
213
223
 
214
 
def make_log_request_dict(direction='reverse', specific_files=None,
 
224
def make_log_request_dict(direction='reverse', specific_fileids=None,
215
225
                          start_revision=None, end_revision=None, limit=None,
216
226
                          message_search=None, levels=None, generate_tags=True,
217
227
                          delta_type=None,
228
238
    :param direction: 'reverse' (default) is latest to earliest;
229
239
      'forward' is earliest to latest.
230
240
 
231
 
    :param specific_files: If not None, only include revisions
 
241
    :param specific_fileids: If not None, only include revisions
232
242
      affecting the specified files, rather than all revisions.
233
243
 
234
244
    :param start_revision: If not None, only generate
251
261
`
252
262
    :param delta_type: Either 'full', 'partial' or None.
253
263
      'full' means generate the complete delta - adds/deletes/modifies/etc;
254
 
      'partial' means filter the delta using specific_files;
 
264
      'partial' means filter the delta using specific_fileids;
255
265
      None means do not generate any delta.
256
266
 
257
267
    :param diff_type: Either 'full', 'partial' or None.
258
268
      'full' means generate the complete diff - adds/deletes/modifies/etc;
259
 
      'partial' means filter the diff using specific_files;
 
269
      'partial' means filter the diff using specific_fileids;
260
270
      None means do not generate any diff.
261
271
 
262
272
    :param _match_using_deltas: a private parameter controlling the
263
 
      algorithm used for matching specific_files. This parameter
 
273
      algorithm used for matching specific_fileids. This parameter
264
274
      may be removed in the future so breezy client code should NOT
265
275
      use it.
266
276
 
285
295
            else:
286
296
                match['message'] = [message_search]
287
297
        else:
288
 
            match = {'message': [message_search]}
 
298
            match={ 'message': [message_search] }
289
299
    return {
290
300
        'direction': direction,
291
 
        'specific_files': specific_files,
 
301
        'specific_fileids': specific_fileids,
292
302
        'start_revision': start_revision,
293
303
        'end_revision': end_revision,
294
304
        'limit': limit,
313
323
    return result
314
324
 
315
325
 
316
 
def format_signature_validity(rev_id, branch):
 
326
def format_signature_validity(rev_id, repo):
317
327
    """get the signature validity
318
328
 
319
329
    :param rev_id: revision id to validate
320
 
    :param branch: branch of revision
 
330
    :param repo: repository of revision
321
331
    :return: human readable string to print to log
322
332
    """
323
333
    from breezy import gpg
324
334
 
325
 
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
326
 
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
 
335
    gpg_strategy = gpg.GPGStrategy(None)
 
336
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
327
337
    if result[0] == gpg.SIGNATURE_VALID:
328
338
        return u"valid signature from {0}".format(result[1])
329
339
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
366
376
        if not isinstance(lf, LogFormatter):
367
377
            warn("not a LogFormatter instance: %r" % lf)
368
378
 
369
 
        with self.branch.lock_read():
 
379
        self.branch.lock_read()
 
380
        try:
370
381
            if getattr(lf, 'begin_log', None):
371
382
                lf.begin_log()
372
383
            self._show_body(lf)
373
384
            if getattr(lf, 'end_log', None):
374
385
                lf.end_log()
 
386
        finally:
 
387
            self.branch.unlock()
375
388
 
376
389
    def _show_body(self, lf):
377
390
        """Show the main log output.
400
413
            for lr in generator.iter_log_revisions():
401
414
                lf.log_revision(lr)
402
415
        except errors.GhostRevisionUnusableHere:
403
 
            raise errors.CommandError(
404
 
                gettext('Further revision history missing.'))
 
416
            raise errors.BzrCommandError(
 
417
                    gettext('Further revision history missing.'))
405
418
        lf.show_advice()
406
419
 
407
420
    def _generator_factory(self, branch, rqst):
409
422
 
410
423
        Subclasses may wish to override this.
411
424
        """
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()
 
425
        return _DefaultLogGenerator(branch, rqst)
479
426
 
480
427
 
481
428
class _StartNotLinearAncestor(Exception):
485
432
class _DefaultLogGenerator(LogGenerator):
486
433
    """The default generator of log revisions."""
487
434
 
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):
 
435
    def __init__(self, branch, rqst):
495
436
        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():
 
437
        self.rqst = rqst
 
438
        if rqst.get('generate_tags') and branch.supports_tags():
510
439
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
511
440
        else:
512
441
            self.rev_tag_dict = {}
516
445
 
517
446
        :return: An iterator yielding LogRevision objects.
518
447
        """
 
448
        rqst = self.rqst
 
449
        levels = rqst.get('levels')
 
450
        limit = rqst.get('limit')
 
451
        diff_type = rqst.get('diff_type')
 
452
        show_signature = rqst.get('signature')
 
453
        omit_merges = rqst.get('omit_merges')
519
454
        log_count = 0
520
455
        revision_iterator = self._create_log_revision_iterator()
521
456
        for revs in revision_iterator:
522
457
            for (rev_id, revno, merge_depth), rev, delta in revs:
523
458
                # 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):
 
459
                if levels != 0 and merge_depth >= levels:
526
460
                    continue
527
 
                if self.omit_merges and len(rev.parent_ids) > 1:
 
461
                if omit_merges and len(rev.parent_ids) > 1:
528
462
                    continue
529
463
                if rev is None:
530
464
                    raise errors.GhostRevisionUnusableHere(rev_id)
531
 
                if self.diff_type is None:
 
465
                if diff_type is None:
532
466
                    diff = None
533
467
                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)
 
468
                    diff = self._format_diff(rev, rev_id, diff_type)
 
469
                if show_signature:
 
470
                    signature = format_signature_validity(rev_id,
 
471
                                                self.branch.repository)
539
472
                else:
540
473
                    signature = None
541
 
                yield LogRevision(
542
 
                    rev, revno, merge_depth, delta,
 
474
                yield LogRevision(rev, revno, merge_depth, delta,
543
475
                    self.rev_tag_dict.get(rev_id), diff, signature)
544
 
                if self.limit:
 
476
                if limit:
545
477
                    log_count += 1
546
 
                    if log_count >= self.limit:
 
478
                    if log_count >= limit:
547
479
                        return
548
480
 
 
481
    def _format_diff(self, rev, rev_id, diff_type):
 
482
        repo = self.branch.repository
 
483
        if len(rev.parent_ids) == 0:
 
484
            ancestor_id = _mod_revision.NULL_REVISION
 
485
        else:
 
486
            ancestor_id = rev.parent_ids[0]
 
487
        tree_1 = repo.revision_tree(ancestor_id)
 
488
        tree_2 = repo.revision_tree(rev_id)
 
489
        file_ids = self.rqst.get('specific_fileids')
 
490
        if diff_type == 'partial' and file_ids is not None:
 
491
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
492
        else:
 
493
            specific_files = None
 
494
        s = BytesIO()
 
495
        path_encoding = get_diff_header_encoding()
 
496
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
497
            new_label='', path_encoding=path_encoding)
 
498
        return s.getvalue()
 
499
 
549
500
    def _create_log_revision_iterator(self):
550
501
        """Create a revision iterator for log.
551
502
 
552
503
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
553
504
            delta).
554
505
        """
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)
 
506
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
507
            self.branch, self.rqst.get('start_revision'),
 
508
            self.rqst.get('end_revision'))
 
509
        if self.rqst.get('_match_using_deltas'):
 
510
            return self._log_revision_iterator_using_delta_matching()
568
511
        else:
569
512
            # We're using the per-file-graph algorithm. This scales really
570
513
            # well but only makes sense if there is a single file and it's
571
514
            # not a directory
572
 
            file_count = len(self.specific_files)
 
515
            file_count = len(self.rqst.get('specific_fileids'))
573
516
            if file_count != 1:
574
 
                raise errors.BzrError(
575
 
                    "illegal LogRequest: must match-using-deltas "
 
517
                raise BzrError("illegal LogRequest: must match-using-deltas "
576
518
                    "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
 
                )
 
519
            return self._log_revision_iterator_using_per_file_graph()
 
520
 
 
521
    def _log_revision_iterator_using_delta_matching(self):
 
522
        # Get the base revisions, filtering by the revision range
 
523
        rqst = self.rqst
 
524
        generate_merge_revisions = rqst.get('levels') != 1
 
525
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
526
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
527
        view_revisions = _calc_view_revisions(
 
528
            self.branch, self.start_rev_id, self.end_rev_id,
 
529
            rqst.get('direction'),
 
530
            generate_merge_revisions=generate_merge_revisions,
 
531
            delayed_graph_generation=delayed_graph_generation,
 
532
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
533
 
 
534
        # Apply the other filters
 
535
        return make_log_rev_iterator(self.branch, view_revisions,
 
536
            rqst.get('delta_type'), rqst.get('match'),
 
537
            file_ids=rqst.get('specific_fileids'),
 
538
            direction=rqst.get('direction'))
 
539
 
 
540
    def _log_revision_iterator_using_per_file_graph(self):
 
541
        # Get the base revisions, filtering by the revision range.
 
542
        # Note that we always generate the merge revisions because
 
543
        # filter_revisions_touching_file_id() requires them ...
 
544
        rqst = self.rqst
 
545
        view_revisions = _calc_view_revisions(
 
546
            self.branch, self.start_rev_id, self.end_rev_id,
 
547
            rqst.get('direction'), generate_merge_revisions=True,
 
548
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
549
        if not isinstance(view_revisions, list):
 
550
            view_revisions = list(view_revisions)
 
551
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
552
            rqst.get('specific_fileids')[0], view_revisions,
 
553
            include_merges=rqst.get('levels') != 1)
 
554
        return make_log_rev_iterator(self.branch, view_revisions,
 
555
            rqst.get('delta_type'), rqst.get('match'))
587
556
 
588
557
 
589
558
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
597
566
             a list of the same tuples.
598
567
    """
599
568
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
 
        raise errors.CommandError(gettext(
 
569
        raise errors.BzrCommandError(gettext(
601
570
            '--exclude-common-ancestry requires two different revisions'))
602
571
    if direction not in ('reverse', 'forward'):
603
572
        raise ValueError(gettext('invalid direction %r') % direction)
604
 
    br_rev_id = branch.last_revision()
605
 
    if br_rev_id == _mod_revision.NULL_REVISION:
 
573
    br_revno, br_rev_id = branch.last_revision_info()
 
574
    if br_revno == 0:
606
575
        return []
607
576
 
608
577
    if (end_rev_id and start_rev_id == end_rev_id
609
578
        and (not generate_merge_revisions
610
579
             or not _has_merges(branch, end_rev_id))):
611
580
        # 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())
 
581
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
582
                                       br_revno)
614
583
    if not generate_merge_revisions:
615
584
        try:
616
585
            # If we only want to see linear revisions, we can iterate ...
621
590
            # ancestor of the end limit, check it before outputting anything
622
591
            if (direction == 'forward'
623
592
                or (start_rev_id and not _is_obvious_ancestor(
624
 
                    branch, start_rev_id, end_rev_id))):
625
 
                iter_revs = list(iter_revs)
 
593
                        branch, start_rev_id, end_rev_id))):
 
594
                    iter_revs = list(iter_revs)
626
595
            if direction == 'forward':
627
596
                iter_revs = reversed(iter_revs)
628
597
            return iter_revs
660
629
    initial_revisions = []
661
630
    if delayed_graph_generation:
662
631
        try:
663
 
            for rev_id, revno, depth in _linear_view_revisions(
664
 
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
632
            for rev_id, revno, depth in  _linear_view_revisions(
 
633
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
665
634
                if _has_merges(branch, rev_id):
666
635
                    # The end_rev_id can be nested down somewhere. We need an
667
636
                    # explicit ancestry check. There is an ambiguity here as we
674
643
                    # -- vila 20100319
675
644
                    graph = branch.repository.get_graph()
676
645
                    if (start_rev_id is not None
677
 
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
646
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
678
647
                        raise _StartNotLinearAncestor()
679
648
                    # Since we collected the revisions so far, we need to
680
649
                    # adjust end_rev_id.
688
657
        except _StartNotLinearAncestor:
689
658
            # A merge was never detected so the lower revision limit can't
690
659
            # be nested down somewhere
691
 
            raise errors.CommandError(gettext('Start revision not found in'
692
 
                                                 ' history of end revision.'))
 
660
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
661
                ' history of end revision.'))
693
662
 
694
663
    # We exit the loop above because we encounter a revision with merges, from
695
664
    # this revision, we need to switch to _graph_view_revisions.
700
669
    # make forward the exact opposite display, but showing the merge revisions
701
670
    # indented at the end seems slightly nicer in that case.
702
671
    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))
 
672
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
673
                              rebase_initial_depths=(direction == 'reverse'),
 
674
                              exclude_common_ancestry=exclude_common_ancestry))
707
675
    return view_revisions
708
676
 
709
677
 
741
709
            # both on mainline
742
710
            return start_dotted[0] <= end_dotted[0]
743
711
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
744
 
              start_dotted[0:1] == end_dotted[0:1]):
 
712
            start_dotted[0:1] == end_dotted[0:1]):
745
713
            # both on same development line
746
714
            return start_dotted[2] <= end_dotted[2]
747
715
        else:
765
733
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
766
734
        is not found walking the left-hand history
767
735
    """
 
736
    br_revno, br_rev_id = branch.last_revision_info()
768
737
    repo = branch.repository
769
738
    graph = repo.get_graph()
770
739
    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
 
 
 
740
        cur_revno = br_revno
784
741
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
 
                                                  (_mod_revision.NULL_REVISION,))
 
742
            (_mod_revision.NULL_REVISION,))
786
743
        while True:
787
744
            try:
788
745
                revision_id = next(graph_iter)
790
747
                # Oops, a ghost.
791
748
                yield e.revision_id, None, None
792
749
                break
793
 
            except StopIteration:
794
 
                break
795
750
            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
 
751
                yield revision_id, str(cur_revno), 0
 
752
                cur_revno -= 1
799
753
    else:
800
 
        br_rev_id = branch.last_revision()
801
754
        if end_rev_id is None:
802
755
            end_rev_id = br_rev_id
803
756
        found_start = start_rev_id is None
804
757
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
805
 
                                                  (_mod_revision.NULL_REVISION,))
 
758
            (_mod_revision.NULL_REVISION,))
806
759
        while True:
807
760
            try:
808
761
                revision_id = next(graph_iter)
873
826
    """Adjust depths upwards so the top level is 0."""
874
827
    # If either the first or last revision have a merge_depth of 0, we're done
875
828
    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])
 
829
        min_depth = min([d for r,n,d in view_revisions])
877
830
        if min_depth != 0:
878
 
            view_revisions = [(r, n, d - min_depth)
879
 
                              for r, n, d in view_revisions]
 
831
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
880
832
    return view_revisions
881
833
 
882
834
 
883
835
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
884
 
                          files=None, direction='reverse'):
 
836
        file_ids=None, direction='reverse'):
885
837
    """Create a revision iterator for log.
886
838
 
887
839
    :param branch: The branch being logged.
889
841
    :param generate_delta: Whether to generate a delta for each revision.
890
842
      Permitted values are None, 'full' and 'partial'.
891
843
    :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.
 
844
    :param file_ids: If non empty, only revisions matching one or more of
 
845
      the file-ids are to be kept.
894
846
    :param direction: the direction in which view_revisions is sorted
895
847
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
896
848
        delta).
911
863
        # It would be nicer if log adapters were first class objects
912
864
        # with custom parameters. This will do for now. IGC 20090127
913
865
        if adapter == _make_delta_filter:
914
 
            log_rev_iterator = adapter(
915
 
                branch, generate_delta, search, log_rev_iterator, files,
916
 
                direction)
 
866
            log_rev_iterator = adapter(branch, generate_delta,
 
867
                search, log_rev_iterator, file_ids, direction)
917
868
        else:
918
 
            log_rev_iterator = adapter(
919
 
                branch, generate_delta, search, log_rev_iterator)
 
869
            log_rev_iterator = adapter(branch, generate_delta,
 
870
                search, log_rev_iterator)
920
871
    return log_rev_iterator
921
872
 
922
873
 
936
887
    """
937
888
    if not match:
938
889
        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])
 
890
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
941
891
                for k, v in match.items()]
942
892
    return _filter_re(searchRE, log_rev_iterator)
943
893
 
948
898
        if new_revs:
949
899
            yield new_revs
950
900
 
951
 
 
952
901
def _match_filter(searchRE, rev):
953
902
    strings = {
954
 
        'message': (rev.message,),
955
 
        'committer': (rev.committer,),
956
 
        'author': (rev.get_apparent_authors()),
957
 
        'bugs': list(rev.iter_bugs())
958
 
        }
 
903
               'message': (rev.message,),
 
904
               'committer': (rev.committer,),
 
905
               'author': (rev.get_apparent_authors()),
 
906
               'bugs': list(rev.iter_bugs())
 
907
               }
959
908
    strings[''] = [item for inner_list in strings.values()
960
909
                   for item in inner_list]
961
 
    for k, v in searchRE:
 
910
    for (k,v) in searchRE:
962
911
        if k in strings and not _match_any_filter(strings[k], v):
963
912
            return False
964
913
    return True
965
914
 
966
 
 
967
915
def _match_any_filter(strings, res):
968
 
    return any(r.search(s) for r in res for s in strings)
969
 
 
 
916
    return any(re.search(s) for re in res for s in strings)
970
917
 
971
918
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
 
                       files=None, direction='reverse'):
 
919
    fileids=None, direction='reverse'):
973
920
    """Add revision deltas to a log iterator if needed.
974
921
 
975
922
    :param branch: The branch being logged.
978
925
    :param search: A user text search string.
979
926
    :param log_rev_iterator: An input iterator containing all revisions that
980
927
        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.
 
928
    :param fileids: If non empty, only revisions matching one or more of
 
929
      the file-ids are to be kept.
983
930
    :param direction: the direction in which view_revisions is sorted
984
931
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
985
932
        delta).
986
933
    """
987
 
    if not generate_delta and not files:
 
934
    if not generate_delta and not fileids:
988
935
        return log_rev_iterator
989
936
    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):
 
937
        generate_delta, fileids, direction)
 
938
 
 
939
 
 
940
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
 
941
    direction):
995
942
    """Create deltas for each batch of revisions in log_rev_iterator.
996
943
 
997
944
    If we're only generating deltas for the sake of filtering against
998
 
    files, we stop generating deltas once all files reach the
 
945
    file-ids, we stop generating deltas once all file-ids reach the
999
946
    appropriate life-cycle point. If we're receiving data newest to
1000
947
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1001
948
    """
1002
 
    check_files = files is not None and len(files) > 0
1003
 
    if check_files:
1004
 
        file_set = set(files)
 
949
    check_fileids = fileids is not None and len(fileids) > 0
 
950
    if check_fileids:
 
951
        fileid_set = set(fileids)
1005
952
        if direction == 'reverse':
1006
953
            stop_on = 'add'
1007
954
        else:
1008
955
            stop_on = 'remove'
1009
956
    else:
1010
 
        file_set = None
 
957
        fileid_set = None
1011
958
    for revs in log_rev_iterator:
1012
 
        # If we were matching against files and we've run out,
 
959
        # If we were matching against fileids and we've run out,
1013
960
        # there's nothing left to do
1014
 
        if check_files and not file_set:
 
961
        if check_fileids and not fileid_set:
1015
962
            return
1016
963
        revisions = [rev[1] for rev in revs]
1017
964
        new_revs = []
1018
 
        if delta_type == 'full' and not check_files:
1019
 
            deltas = repository.get_revision_deltas(revisions)
 
965
        if delta_type == 'full' and not check_fileids:
 
966
            deltas = repository.get_deltas_for_revisions(revisions)
1020
967
            for rev, delta in zip(revs, deltas):
1021
968
                new_revs.append((rev[0], rev[1], delta))
1022
969
        else:
1023
 
            deltas = repository.get_revision_deltas(
1024
 
                revisions, specific_files=file_set)
 
970
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
1025
971
            for rev, delta in zip(revs, deltas):
1026
 
                if check_files:
 
972
                if check_fileids:
1027
973
                    if delta is None or not delta.has_changed():
1028
974
                        continue
1029
975
                    else:
1030
 
                        _update_files(delta, file_set, stop_on)
 
976
                        _update_fileids(delta, fileid_set, stop_on)
1031
977
                        if delta_type is None:
1032
978
                            delta = None
1033
979
                        elif delta_type == 'full':
1044
990
        yield new_revs
1045
991
 
1046
992
 
1047
 
def _update_files(delta, files, stop_on):
1048
 
    """Update the set of files to search based on file lifecycle events.
 
993
def _update_fileids(delta, fileids, stop_on):
 
994
    """Update the set of file-ids to search based on file lifecycle events.
1049
995
 
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
 
996
    :param fileids: a set of fileids to update
 
997
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
998
      fileids set once their add or remove entry is detected respectively
1053
999
    """
1054
1000
    if stop_on == 'add':
1055
1001
        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]):])
 
1002
            if item[1] in fileids:
 
1003
                fileids.remove(item[1])
1067
1004
    elif stop_on == 'delete':
1068
1005
        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]):])
 
1006
            if item[1] in fileids:
 
1007
                fileids.remove(item[1])
1080
1008
 
1081
1009
 
1082
1010
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1123
1051
def _get_revision_limits(branch, start_revision, end_revision):
1124
1052
    """Get and check revision limits.
1125
1053
 
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
 
1054
    :param  branch: The branch containing the revisions.
 
1055
 
 
1056
    :param  start_revision: The first revision to be logged.
 
1057
            For backwards compatibility this may be a mainline integer revno,
 
1058
            but for merge revision support a RevisionInfo is expected.
 
1059
 
 
1060
    :param  end_revision: The last revision to be logged.
 
1061
            For backwards compatibility this may be a mainline integer revno,
 
1062
            but for merge revision support a RevisionInfo is expected.
1131
1063
 
1132
1064
    :return: (start_rev_id, end_rev_id) tuple.
1133
1065
    """
 
1066
    branch_revno, branch_rev_id = branch.last_revision_info()
1134
1067
    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:
 
1068
    if start_revision is None:
1142
1069
        start_revno = 1
 
1070
    else:
 
1071
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
1072
            start_rev_id = start_revision.rev_id
 
1073
            start_revno = start_revision.revno or 1
 
1074
        else:
 
1075
            branch.check_real_revno(start_revision)
 
1076
            start_revno = start_revision
 
1077
            start_rev_id = branch.get_rev_id(start_revno)
1143
1078
 
1144
1079
    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
 
1080
    if end_revision is None:
 
1081
        end_revno = branch_revno
 
1082
    else:
 
1083
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
1084
            end_rev_id = end_revision.rev_id
 
1085
            end_revno = end_revision.revno or branch_revno
 
1086
        else:
 
1087
            branch.check_real_revno(end_revision)
 
1088
            end_revno = end_revision
 
1089
            end_rev_id = branch.get_rev_id(end_revno)
1151
1090
 
1152
 
    if branch.last_revision() != _mod_revision.NULL_REVISION:
 
1091
    if branch_revno != 0:
1153
1092
        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."))
 
1093
            or end_rev_id == _mod_revision.NULL_REVISION):
 
1094
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1095
        if start_revno > end_revno:
 
1096
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1097
                                         "older than the end revision."))
1160
1098
    return (start_rev_id, end_rev_id)
1161
1099
 
1162
1100
 
1210
1148
            end_revno = end_revision
1211
1149
 
1212
1150
    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.'))
 
1151
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1152
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1215
1153
    if start_revno > end_revno:
1216
 
        raise errors.CommandError(gettext("Start revision must be older "
1217
 
                                             "than the end revision."))
 
1154
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1155
                                     "than the end revision."))
1218
1156
 
1219
1157
    if end_revno < start_revno:
1220
1158
        return None, None, None, None
1243
1181
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1244
1182
 
1245
1183
 
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.
 
1184
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1185
    include_merges=True):
 
1186
    r"""Return the list of revision ids which touch a given file id.
1249
1187
 
1250
1188
    The function filters view_revisions and returns a subset.
1251
 
    This includes the revisions which directly change the path,
 
1189
    This includes the revisions which directly change the file id,
1252
1190
    and the revisions which merge these changes. So if the
1253
1191
    revision graph is::
1254
1192
 
1271
1209
 
1272
1210
    :param branch: The branch where we can get text revision information.
1273
1211
 
1274
 
    :param path: Filter out revisions that do not touch path.
 
1212
    :param file_id: Filter out revisions that do not touch file_id.
1275
1213
 
1276
1214
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1277
1215
        tuples. This is the list of revisions which will be filtered. It is
1285
1223
    # Lookup all possible text keys to determine which ones actually modified
1286
1224
    # the file.
1287
1225
    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
1226
    get_parent_map = graph.get_parent_map
1291
1227
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1292
1228
    next_keys = None
1333
1269
    """Reverse revisions by depth.
1334
1270
 
1335
1271
    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
 
1272
    revision of that depth.  There may be no topological justification for this,
1337
1273
    but it looks much nicer.
1338
1274
    """
1339
1275
    # Add a fake revision at start so that we can always attach sub revisions
1446
1382
        """
1447
1383
        self.to_file = to_file
1448
1384
        # '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
 
1385
        # and should not try to decode/encode it to unicode to avoid bug #328007
1451
1386
        if to_exact_file is not None:
1452
1387
            self.to_exact_file = to_exact_file
1453
1388
        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
 
1389
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1390
            # for code that expects to get diffs to pass in the exact file
 
1391
            # stream
1457
1392
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1458
1393
        self.show_ids = show_ids
1459
1394
        self.show_timezone = show_timezone
1460
1395
        if delta_format is None:
1461
1396
            # Ensures backward compatibility
1462
 
            delta_format = 2  # long format
 
1397
            delta_format = 2 # long format
1463
1398
        self.delta_format = delta_format
1464
1399
        self.levels = levels
1465
1400
        self._show_advice = show_advice
1563
1498
        """
1564
1499
        lines = self._foreign_info_properties(revision)
1565
1500
        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)
 
1501
            lines.extend(self._format_properties(handler(revision)))
1571
1502
        return lines
1572
1503
 
1573
1504
    def _foreign_info_properties(self, rev):
1581
1512
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1582
1513
 
1583
1514
        # Imported foreign revision revision ids always contain :
1584
 
        if b":" not in rev.revision_id:
 
1515
        if not ":" in rev.revision_id:
1585
1516
            return []
1586
1517
 
1587
1518
        # Revision was once imported from a foreign repository
1601
1532
        return lines
1602
1533
 
1603
1534
    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')
 
1535
        for l in diff.rstrip().split('\n'):
 
1536
            to_file.write(indent + '%s\n' % (l,))
1607
1537
 
1608
1538
 
1609
1539
# Separator between revisions in long format
1632
1562
 
1633
1563
    def _date_string_original_timezone(self, rev):
1634
1564
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1635
 
                                                            rev.timezone or 0)
 
1565
            rev.timezone or 0)
1636
1566
 
1637
1567
    def log_revision(self, revision):
1638
1568
        """Log a revision, either merged or not."""
1640
1570
        lines = [_LONG_SEP]
1641
1571
        if revision.revno is not None:
1642
1572
            lines.append('revno: %s%s' % (revision.revno,
1643
 
                                          self.merge_marker(revision)))
 
1573
                self.merge_marker(revision)))
1644
1574
        if revision.tags:
1645
 
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1575
            lines.append('tags: %s' % (', '.join(revision.tags)))
1646
1576
        if self.show_ids or revision.revno is None:
1647
 
            lines.append('revision-id: %s' %
1648
 
                         (revision.rev.revision_id.decode('utf-8'),))
 
1577
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1649
1578
        if self.show_ids:
1650
1579
            for parent_id in revision.rev.parent_ids:
1651
 
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1580
                lines.append('parent: %s' % (parent_id,))
1652
1581
        lines.extend(self.custom_properties(revision.rev))
1653
1582
 
1654
1583
        committer = revision.rev.committer
1730
1659
        to_file = self.to_file
1731
1660
        tags = ''
1732
1661
        if revision.tags:
1733
 
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1662
            tags = ' {%s}' % (', '.join(revision.tags))
1734
1663
        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)
 
1664
                revision.revno or "", self.short_author(revision.rev),
 
1665
                format_date(revision.rev.timestamp,
 
1666
                            revision.rev.timezone or 0,
 
1667
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
1668
                            show_offset=False),
 
1669
                tags, self.merge_marker(revision)))
 
1670
        self.show_properties(revision.rev, indent+offset)
1743
1671
        if self.show_ids or revision.revno is None:
1744
1672
            to_file.write(indent + offset + 'revision-id:%s\n'
1745
 
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1673
                          % (revision.rev.revision_id,))
1746
1674
        if not revision.rev.message:
1747
1675
            to_file.write(indent + offset + '(no message)\n')
1748
1676
        else:
1754
1682
            # Use the standard status output to display changes
1755
1683
            from breezy.delta import report_delta
1756
1684
            report_delta(to_file, revision.delta,
1757
 
                         short_status=self.delta_format == 1,
 
1685
                         short_status=self.delta_format==1,
1758
1686
                         show_ids=self.show_ids, indent=indent + offset)
1759
1687
        if revision.diff is not None:
1760
1688
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1778
1706
    def truncate(self, str, max_len):
1779
1707
        if max_len is None or len(str) <= max_len:
1780
1708
            return str
1781
 
        return str[:max_len - 3] + '...'
 
1709
        return str[:max_len-3] + '...'
1782
1710
 
1783
1711
    def date_string(self, rev):
1784
1712
        return format_date(rev.timestamp, rev.timezone or 0,
1794
1722
    def log_revision(self, revision):
1795
1723
        indent = '  ' * revision.merge_depth
1796
1724
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1797
 
                                           self._max_chars, revision.tags, indent))
 
1725
            self._max_chars, revision.tags, indent))
1798
1726
        self.to_file.write('\n')
1799
1727
 
1800
1728
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1813
1741
            # show revno only when is not None
1814
1742
            out.append("%s:" % revno)
1815
1743
        if max_chars is not None:
1816
 
            out.append(self.truncate(
1817
 
                self.short_author(rev), (max_chars + 3) // 4))
 
1744
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1818
1745
        else:
1819
1746
            out.append(self.short_author(rev))
1820
1747
        out.append(self.date_string(rev))
1821
1748
        if len(rev.parent_ids) > 1:
1822
1749
            out.append('[merge]')
1823
1750
        if tags:
1824
 
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1751
            tag_str = '{%s}' % (', '.join(tags))
1825
1752
            out.append(tag_str)
1826
1753
        out.append(rev.get_summary())
1827
1754
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1843
1770
                               show_offset=False)
1844
1771
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1845
1772
        committer_str = committer_str.replace(' <', '  <')
1846
 
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
 
1773
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1847
1774
 
1848
1775
        if revision.delta is not None and revision.delta.has_changed():
1849
1776
            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]
 
1777
                path, = c[:1]
1854
1778
                to_file.write('\t* %s:\n' % (path,))
1855
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1779
            for c in revision.delta.renamed:
 
1780
                oldpath,newpath = c[:2]
1856
1781
                # 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]))
 
1782
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
1858
1783
            to_file.write('\n')
1859
1784
 
1860
1785
        if not revision.rev.message:
1913
1838
    try:
1914
1839
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1915
1840
    except KeyError:
1916
 
        raise errors.CommandError(
1917
 
            gettext("unknown log formatter: %r") % name)
 
1841
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1918
1842
 
1919
1843
 
1920
1844
def author_list_all(rev):
1956
1880
    """
1957
1881
    if to_file is None:
1958
1882
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1959
 
                                                            errors='replace')
 
1883
            errors='replace')
1960
1884
    lf = log_formatter(log_format,
1961
1885
                       show_ids=False,
1962
1886
                       to_file=to_file,
1968
1892
    for i in range(max(len(new_rh), len(old_rh))):
1969
1893
        if (len(new_rh) <= i
1970
1894
            or len(old_rh) <= i
1971
 
                or new_rh[i] != old_rh[i]):
 
1895
            or new_rh[i] != old_rh[i]):
1972
1896
            base_idx = i
1973
1897
            break
1974
1898
 
1975
1899
    if base_idx is None:
1976
1900
        to_file.write('Nothing seems to have changed\n')
1977
1901
        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
 
1902
    ## TODO: It might be nice to do something like show_log
 
1903
    ##       and show the merged entries. But since this is the
 
1904
    ##       removed revisions, it shouldn't be as important
1981
1905
    if base_idx < len(old_rh):
1982
 
        to_file.write('*' * 60)
 
1906
        to_file.write('*'*60)
1983
1907
        to_file.write('\nRemoved Revisions:\n')
1984
1908
        for i in range(base_idx, len(old_rh)):
1985
1909
            rev = branch.repository.get_revision(old_rh[i])
1986
 
            lr = LogRevision(rev, i + 1, 0, None)
 
1910
            lr = LogRevision(rev, i+1, 0, None)
1987
1911
            lf.log_revision(lr)
1988
 
        to_file.write('*' * 60)
 
1912
        to_file.write('*'*60)
1989
1913
        to_file.write('\n\n')
1990
1914
    if base_idx < len(new_rh):
1991
1915
        to_file.write('Added Revisions:\n')
1992
1916
        show_log(branch,
1993
1917
                 lf,
 
1918
                 None,
1994
1919
                 verbose=False,
1995
1920
                 direction='forward',
1996
 
                 start_revision=base_idx + 1,
1997
 
                 end_revision=len(new_rh))
 
1921
                 start_revision=base_idx+1,
 
1922
                 end_revision=len(new_rh),
 
1923
                 search=None)
1998
1924
 
1999
1925
 
2000
1926
def get_history_change(old_revision_id, new_revision_id, repository):
2066
1992
    log_format = log_formatter_registry.get_default(branch)
2067
1993
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
2068
1994
    if old_history != []:
2069
 
        output.write('*' * 60)
 
1995
        output.write('*'*60)
2070
1996
        output.write('\nRemoved Revisions:\n')
2071
1997
        show_flat_log(branch.repository, old_history, old_revno, lf)
2072
 
        output.write('*' * 60)
 
1998
        output.write('*'*60)
2073
1999
        output.write('\n\n')
2074
2000
    if new_history != []:
2075
2001
        output.write('Added Revisions:\n')
2076
2002
        start_revno = new_revno - len(new_history) + 1
2077
 
        show_log(branch, lf, verbose=False, direction='forward',
2078
 
                 start_revision=start_revno)
 
2003
        show_log(branch, lf, None, verbose=False, direction='forward',
 
2004
                 start_revision=start_revno,)
2079
2005
 
2080
2006
 
2081
2007
def show_flat_log(repository, history, last_revno, lf):
2086
2012
    :param last_revno: The revno of the last revision_id in the history.
2087
2013
    :param lf: The log formatter to use.
2088
2014
    """
 
2015
    start_revno = last_revno - len(history) + 1
2089
2016
    revisions = repository.get_revisions(history)
2090
2017
    for i, rev in enumerate(revisions):
2091
2018
        lr = LogRevision(rev, i + last_revno, 0, None)
2092
2019
        lf.log_revision(lr)
2093
2020
 
2094
2021
 
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.
 
2022
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2023
    """Find file-ids and kinds given a list of files and a revision range.
2097
2024
 
2098
2025
    We search for files at the end of the range. If not found there,
2099
2026
    we try the start of the range.
2102
2029
    :param file_list: the list of paths given on the command line;
2103
2030
      the first of these can be a branch location or a file path,
2104
2031
      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.
 
2032
    :param add_cleanup: When the branch returned is read locked,
 
2033
      an unlock call will be queued to the cleanup.
2107
2034
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2108
 
      info_list is a list of (relative_path, found, kind) tuples where
 
2035
      info_list is a list of (relative_path, file_id, kind) tuples where
2109
2036
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2110
2037
      branch will be read-locked.
2111
2038
    """
2112
2039
    from breezy.builtins import _get_revision_range
2113
2040
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2114
2041
        file_list[0])
2115
 
    exit_stack.enter_context(b.lock_read())
 
2042
    add_cleanup(b.lock_read().unlock)
2116
2043
    # XXX: It's damn messy converting a list of paths to relative paths when
2117
2044
    # those paths might be deleted ones, they might be on a case-insensitive
2118
2045
    # filesystem and/or they might be in silly locations (like another branch).
2127
2054
        relpaths = [path] + file_list[1:]
2128
2055
    info_list = []
2129
2056
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2130
 
                                                       "log")
 
2057
        "log")
2131
2058
    if relpaths in ([], [u'']):
2132
2059
        return b, [], start_rev_info, end_rev_info
2133
2060
    if start_rev_info is None and end_rev_info is None:
2135
2062
            tree = b.basis_tree()
2136
2063
        tree1 = None
2137
2064
        for fp in relpaths:
2138
 
            kind = _get_kind_for_file(tree, fp)
2139
 
            if not kind:
 
2065
            file_id = tree.path2id(fp)
 
2066
            kind = _get_kind_for_file_id(tree, file_id)
 
2067
            if file_id is None:
2140
2068
                # go back to when time began
2141
2069
                if tree1 is None:
2142
2070
                    try:
2143
2071
                        rev1 = b.get_rev_id(1)
2144
2072
                    except errors.NoSuchRevision:
2145
2073
                        # No history at all
 
2074
                        file_id = None
2146
2075
                        kind = None
2147
2076
                    else:
2148
2077
                        tree1 = b.repository.revision_tree(rev1)
2149
2078
                if tree1:
2150
 
                    kind = _get_kind_for_file(tree1, fp)
2151
 
            info_list.append((fp, kind))
 
2079
                    file_id = tree1.path2id(fp)
 
2080
                    kind = _get_kind_for_file_id(tree1, file_id)
 
2081
            info_list.append((fp, file_id, kind))
2152
2082
 
2153
2083
    elif start_rev_info == end_rev_info:
2154
2084
        # One revision given - file must exist in it
2155
2085
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2156
2086
        for fp in relpaths:
2157
 
            kind = _get_kind_for_file(tree, fp)
2158
 
            info_list.append((fp, kind))
 
2087
            file_id = tree.path2id(fp)
 
2088
            kind = _get_kind_for_file_id(tree, file_id)
 
2089
            info_list.append((fp, file_id, kind))
2159
2090
 
2160
2091
    else:
2161
2092
        # Revision range given. Get the file-id from the end tree.
2167
2098
            tree = b.repository.revision_tree(rev_id)
2168
2099
        tree1 = None
2169
2100
        for fp in relpaths:
2170
 
            kind = _get_kind_for_file(tree, fp)
2171
 
            if not kind:
 
2101
            file_id = tree.path2id(fp)
 
2102
            kind = _get_kind_for_file_id(tree, file_id)
 
2103
            if file_id is None:
2172
2104
                if tree1 is None:
2173
2105
                    rev_id = start_rev_info.rev_id
2174
2106
                    if rev_id is None:
2176
2108
                        tree1 = b.repository.revision_tree(rev1)
2177
2109
                    else:
2178
2110
                        tree1 = b.repository.revision_tree(rev_id)
2179
 
                kind = _get_kind_for_file(tree1, fp)
2180
 
            info_list.append((fp, kind))
 
2111
                file_id = tree1.path2id(fp)
 
2112
                kind = _get_kind_for_file_id(tree1, file_id)
 
2113
            info_list.append((fp, file_id, kind))
2181
2114
    return b, info_list, start_rev_info, end_rev_info
2182
2115
 
2183
2116
 
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
 
2117
def _get_kind_for_file_id(tree, file_id):
 
2118
    """Return the kind of a file-id or None if it doesn't exist."""
 
2119
    if file_id is not None:
 
2120
        return tree.kind(file_id)
 
2121
    else:
 
2122
        return None
2191
2123
 
2192
2124
 
2193
2125
properties_handler_registry = registry.Registry()
2194
2126
 
2195
2127
# Use the properties handlers to print out bug information if available
2196
 
 
2197
 
 
2198
2128
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
 
2129
    if 'bugs' in revision.properties:
 
2130
        bug_lines = revision.properties['bugs'].split('\n')
 
2131
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2132
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2133
                          len(row) > 1 and row[1] == 'fixed']
2215
2134
 
 
2135
        if fixed_bug_urls:
 
2136
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2137
                    ' '.join(fixed_bug_urls)}
 
2138
    return {}
2216
2139
 
2217
2140
properties_handler_registry.register('bugs_properties_handler',
2218
2141
                                     _bugs_properties_handler)