/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to breezy/log.py

  • Committer: Jelmer Vernooij
  • Date: 2018-02-18 21:42:57 UTC
  • mto: This revision was merged to the branch mainline in revision 6859.
  • Revision ID: jelmer@jelmer.uk-20180218214257-jpevutp1wa30tz3v
Update TODO to reference Breezy, not Bazaar.

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
    tsort,
69
72
    )
70
73
from breezy.i18n import gettext, ngettext
71
74
""")
72
75
 
73
76
from . import (
74
 
    errors,
 
77
    lazy_regex,
75
78
    registry,
76
79
    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_verifier = 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_tree = branch.repository.revision_tree(revision_id)
 
114
        try:
 
115
            this_path = this_tree.id2path(file_id)
 
116
        except errors.NoSuchId:
 
117
            this_verifier = this_path = None
 
118
        else:
 
119
            this_verifier = this_tree.get_file_verifier(this_path, file_id)
112
120
 
113
121
        # now we know how it was last time, and how it is in this revision.
114
122
        # 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
 
123
 
 
124
        if not this_verifier and not last_verifier:
 
125
            # not present in either
 
126
            pass
 
127
        elif this_verifier and not last_verifier:
 
128
            yield revno, revision_id, "added " + this_path
 
129
        elif not this_verifier and last_verifier:
 
130
            # deleted here
 
131
            yield revno, revision_id, "deleted " + last_path
120
132
        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
 
133
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
 
134
        elif (this_verifier != last_verifier):
 
135
            yield revno, revision_id, "modified " + this_path
127
136
 
128
137
        last_verifier = this_verifier
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,
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:
 
468
                    diff = self._format_diff(rev, rev_id, diff_type)
 
469
                if show_signature:
538
470
                    signature = format_signature_validity(rev_id, self.branch)
539
471
                else:
540
472
                    signature = None
541
 
                yield LogRevision(
542
 
                    rev, revno, merge_depth, delta,
 
473
                yield LogRevision(rev, revno, merge_depth, delta,
543
474
                    self.rev_tag_dict.get(rev_id), diff, signature)
544
 
                if self.limit:
 
475
                if limit:
545
476
                    log_count += 1
546
 
                    if log_count >= self.limit:
 
477
                    if log_count >= limit:
547
478
                        return
548
479
 
 
480
    def _format_diff(self, rev, rev_id, diff_type):
 
481
        repo = self.branch.repository
 
482
        if len(rev.parent_ids) == 0:
 
483
            ancestor_id = _mod_revision.NULL_REVISION
 
484
        else:
 
485
            ancestor_id = rev.parent_ids[0]
 
486
        tree_1 = repo.revision_tree(ancestor_id)
 
487
        tree_2 = repo.revision_tree(rev_id)
 
488
        file_ids = self.rqst.get('specific_fileids')
 
489
        if diff_type == 'partial' and file_ids is not None:
 
490
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
491
        else:
 
492
            specific_files = None
 
493
        s = BytesIO()
 
494
        path_encoding = get_diff_header_encoding()
 
495
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
496
            new_label='', path_encoding=path_encoding)
 
497
        return s.getvalue()
 
498
 
549
499
    def _create_log_revision_iterator(self):
550
500
        """Create a revision iterator for log.
551
501
 
552
502
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
553
503
            delta).
554
504
        """
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)
 
505
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
506
            self.branch, self.rqst.get('start_revision'),
 
507
            self.rqst.get('end_revision'))
 
508
        if self.rqst.get('_match_using_deltas'):
 
509
            return self._log_revision_iterator_using_delta_matching()
568
510
        else:
569
511
            # We're using the per-file-graph algorithm. This scales really
570
512
            # well but only makes sense if there is a single file and it's
571
513
            # not a directory
572
 
            file_count = len(self.specific_files)
 
514
            file_count = len(self.rqst.get('specific_fileids'))
573
515
            if file_count != 1:
574
 
                raise errors.BzrError(
575
 
                    "illegal LogRequest: must match-using-deltas "
 
516
                raise BzrError("illegal LogRequest: must match-using-deltas "
576
517
                    "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
 
                )
 
518
            return self._log_revision_iterator_using_per_file_graph()
 
519
 
 
520
    def _log_revision_iterator_using_delta_matching(self):
 
521
        # Get the base revisions, filtering by the revision range
 
522
        rqst = self.rqst
 
523
        generate_merge_revisions = rqst.get('levels') != 1
 
524
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
525
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
526
        view_revisions = _calc_view_revisions(
 
527
            self.branch, self.start_rev_id, self.end_rev_id,
 
528
            rqst.get('direction'),
 
529
            generate_merge_revisions=generate_merge_revisions,
 
530
            delayed_graph_generation=delayed_graph_generation,
 
531
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
532
 
 
533
        # Apply the other filters
 
534
        return make_log_rev_iterator(self.branch, view_revisions,
 
535
            rqst.get('delta_type'), rqst.get('match'),
 
536
            file_ids=rqst.get('specific_fileids'),
 
537
            direction=rqst.get('direction'))
 
538
 
 
539
    def _log_revision_iterator_using_per_file_graph(self):
 
540
        # Get the base revisions, filtering by the revision range.
 
541
        # Note that we always generate the merge revisions because
 
542
        # filter_revisions_touching_file_id() requires them ...
 
543
        rqst = self.rqst
 
544
        view_revisions = _calc_view_revisions(
 
545
            self.branch, self.start_rev_id, self.end_rev_id,
 
546
            rqst.get('direction'), generate_merge_revisions=True,
 
547
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
548
        if not isinstance(view_revisions, list):
 
549
            view_revisions = list(view_revisions)
 
550
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
551
            rqst.get('specific_fileids')[0], view_revisions,
 
552
            include_merges=rqst.get('levels') != 1)
 
553
        return make_log_rev_iterator(self.branch, view_revisions,
 
554
            rqst.get('delta_type'), rqst.get('match'))
587
555
 
588
556
 
589
557
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
597
565
             a list of the same tuples.
598
566
    """
599
567
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
 
        raise errors.CommandError(gettext(
 
568
        raise errors.BzrCommandError(gettext(
601
569
            '--exclude-common-ancestry requires two different revisions'))
602
570
    if direction not in ('reverse', 'forward'):
603
571
        raise ValueError(gettext('invalid direction %r') % direction)
604
 
    br_rev_id = branch.last_revision()
605
 
    if br_rev_id == _mod_revision.NULL_REVISION:
 
572
    br_revno, br_rev_id = branch.last_revision_info()
 
573
    if br_revno == 0:
606
574
        return []
607
575
 
608
576
    if (end_rev_id and start_rev_id == end_rev_id
609
577
        and (not generate_merge_revisions
610
578
             or not _has_merges(branch, end_rev_id))):
611
579
        # 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())
 
580
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
581
                                       br_revno)
614
582
    if not generate_merge_revisions:
615
583
        try:
616
584
            # If we only want to see linear revisions, we can iterate ...
621
589
            # ancestor of the end limit, check it before outputting anything
622
590
            if (direction == 'forward'
623
591
                or (start_rev_id and not _is_obvious_ancestor(
624
 
                    branch, start_rev_id, end_rev_id))):
625
 
                iter_revs = list(iter_revs)
 
592
                        branch, start_rev_id, end_rev_id))):
 
593
                    iter_revs = list(iter_revs)
626
594
            if direction == 'forward':
627
595
                iter_revs = reversed(iter_revs)
628
596
            return iter_revs
660
628
    initial_revisions = []
661
629
    if delayed_graph_generation:
662
630
        try:
663
 
            for rev_id, revno, depth in _linear_view_revisions(
664
 
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
631
            for rev_id, revno, depth in  _linear_view_revisions(
 
632
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
665
633
                if _has_merges(branch, rev_id):
666
634
                    # The end_rev_id can be nested down somewhere. We need an
667
635
                    # explicit ancestry check. There is an ambiguity here as we
674
642
                    # -- vila 20100319
675
643
                    graph = branch.repository.get_graph()
676
644
                    if (start_rev_id is not None
677
 
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
645
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
678
646
                        raise _StartNotLinearAncestor()
679
647
                    # Since we collected the revisions so far, we need to
680
648
                    # adjust end_rev_id.
688
656
        except _StartNotLinearAncestor:
689
657
            # A merge was never detected so the lower revision limit can't
690
658
            # be nested down somewhere
691
 
            raise errors.CommandError(gettext('Start revision not found in'
692
 
                                                 ' history of end revision.'))
 
659
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
660
                ' history of end revision.'))
693
661
 
694
662
    # We exit the loop above because we encounter a revision with merges, from
695
663
    # this revision, we need to switch to _graph_view_revisions.
700
668
    # make forward the exact opposite display, but showing the merge revisions
701
669
    # indented at the end seems slightly nicer in that case.
702
670
    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))
 
671
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
672
                              rebase_initial_depths=(direction == 'reverse'),
 
673
                              exclude_common_ancestry=exclude_common_ancestry))
707
674
    return view_revisions
708
675
 
709
676
 
741
708
            # both on mainline
742
709
            return start_dotted[0] <= end_dotted[0]
743
710
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
744
 
              start_dotted[0:1] == end_dotted[0:1]):
 
711
            start_dotted[0:1] == end_dotted[0:1]):
745
712
            # both on same development line
746
713
            return start_dotted[2] <= end_dotted[2]
747
714
        else:
765
732
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
766
733
        is not found walking the left-hand history
767
734
    """
 
735
    br_revno, br_rev_id = branch.last_revision_info()
768
736
    repo = branch.repository
769
737
    graph = repo.get_graph()
770
738
    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
 
 
 
739
        cur_revno = br_revno
784
740
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
 
                                                  (_mod_revision.NULL_REVISION,))
 
741
            (_mod_revision.NULL_REVISION,))
786
742
        while True:
787
743
            try:
788
744
                revision_id = next(graph_iter)
790
746
                # Oops, a ghost.
791
747
                yield e.revision_id, None, None
792
748
                break
793
 
            except StopIteration:
794
 
                break
795
749
            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
 
750
                yield revision_id, str(cur_revno), 0
 
751
                cur_revno -= 1
799
752
    else:
800
 
        br_rev_id = branch.last_revision()
801
753
        if end_rev_id is None:
802
754
            end_rev_id = br_rev_id
803
755
        found_start = start_rev_id is None
804
756
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
805
 
                                                  (_mod_revision.NULL_REVISION,))
 
757
            (_mod_revision.NULL_REVISION,))
806
758
        while True:
807
759
            try:
808
760
                revision_id = next(graph_iter)
875
827
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
876
828
        min_depth = min([d for r, n, d in view_revisions])
877
829
        if min_depth != 0:
878
 
            view_revisions = [(r, n, d - min_depth)
879
 
                              for r, n, d in view_revisions]
 
830
            view_revisions = [(r, n, d-min_depth) for r, n, d in view_revisions]
880
831
    return view_revisions
881
832
 
882
833
 
883
834
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
884
 
                          files=None, direction='reverse'):
 
835
        file_ids=None, direction='reverse'):
885
836
    """Create a revision iterator for log.
886
837
 
887
838
    :param branch: The branch being logged.
889
840
    :param generate_delta: Whether to generate a delta for each revision.
890
841
      Permitted values are None, 'full' and 'partial'.
891
842
    :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.
 
843
    :param file_ids: If non empty, only revisions matching one or more of
 
844
      the file-ids are to be kept.
894
845
    :param direction: the direction in which view_revisions is sorted
895
846
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
896
847
        delta).
911
862
        # It would be nicer if log adapters were first class objects
912
863
        # with custom parameters. This will do for now. IGC 20090127
913
864
        if adapter == _make_delta_filter:
914
 
            log_rev_iterator = adapter(
915
 
                branch, generate_delta, search, log_rev_iterator, files,
916
 
                direction)
 
865
            log_rev_iterator = adapter(branch, generate_delta,
 
866
                search, log_rev_iterator, file_ids, direction)
917
867
        else:
918
 
            log_rev_iterator = adapter(
919
 
                branch, generate_delta, search, log_rev_iterator)
 
868
            log_rev_iterator = adapter(branch, generate_delta,
 
869
                search, log_rev_iterator)
920
870
    return log_rev_iterator
921
871
 
922
872
 
936
886
    """
937
887
    if not match:
938
888
        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])
 
889
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
941
890
                for k, v in match.items()]
942
891
    return _filter_re(searchRE, log_rev_iterator)
943
892
 
948
897
        if new_revs:
949
898
            yield new_revs
950
899
 
951
 
 
952
900
def _match_filter(searchRE, rev):
953
901
    strings = {
954
 
        'message': (rev.message,),
955
 
        'committer': (rev.committer,),
956
 
        'author': (rev.get_apparent_authors()),
957
 
        'bugs': list(rev.iter_bugs())
958
 
        }
 
902
               'message': (rev.message,),
 
903
               'committer': (rev.committer,),
 
904
               'author': (rev.get_apparent_authors()),
 
905
               'bugs': list(rev.iter_bugs())
 
906
               }
959
907
    strings[''] = [item for inner_list in strings.values()
960
908
                   for item in inner_list]
961
 
    for k, v in searchRE:
 
909
    for (k, v) in searchRE:
962
910
        if k in strings and not _match_any_filter(strings[k], v):
963
911
            return False
964
912
    return True
965
913
 
966
 
 
967
914
def _match_any_filter(strings, res):
968
 
    return any(r.search(s) for r in res for s in strings)
969
 
 
 
915
    return any(re.search(s) for re in res for s in strings)
970
916
 
971
917
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
 
                       files=None, direction='reverse'):
 
918
    fileids=None, direction='reverse'):
973
919
    """Add revision deltas to a log iterator if needed.
974
920
 
975
921
    :param branch: The branch being logged.
978
924
    :param search: A user text search string.
979
925
    :param log_rev_iterator: An input iterator containing all revisions that
980
926
        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.
 
927
    :param fileids: If non empty, only revisions matching one or more of
 
928
      the file-ids are to be kept.
983
929
    :param direction: the direction in which view_revisions is sorted
984
930
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
985
931
        delta).
986
932
    """
987
 
    if not generate_delta and not files:
 
933
    if not generate_delta and not fileids:
988
934
        return log_rev_iterator
989
935
    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):
 
936
        generate_delta, fileids, direction)
 
937
 
 
938
 
 
939
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
 
940
    direction):
995
941
    """Create deltas for each batch of revisions in log_rev_iterator.
996
942
 
997
943
    If we're only generating deltas for the sake of filtering against
998
 
    files, we stop generating deltas once all files reach the
 
944
    file-ids, we stop generating deltas once all file-ids reach the
999
945
    appropriate life-cycle point. If we're receiving data newest to
1000
946
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1001
947
    """
1002
 
    check_files = files is not None and len(files) > 0
1003
 
    if check_files:
1004
 
        file_set = set(files)
 
948
    check_fileids = fileids is not None and len(fileids) > 0
 
949
    if check_fileids:
 
950
        fileid_set = set(fileids)
1005
951
        if direction == 'reverse':
1006
952
            stop_on = 'add'
1007
953
        else:
1008
954
            stop_on = 'remove'
1009
955
    else:
1010
 
        file_set = None
 
956
        fileid_set = None
1011
957
    for revs in log_rev_iterator:
1012
 
        # If we were matching against files and we've run out,
 
958
        # If we were matching against fileids and we've run out,
1013
959
        # there's nothing left to do
1014
 
        if check_files and not file_set:
 
960
        if check_fileids and not fileid_set:
1015
961
            return
1016
962
        revisions = [rev[1] for rev in revs]
1017
963
        new_revs = []
1018
 
        if delta_type == 'full' and not check_files:
1019
 
            deltas = repository.get_revision_deltas(revisions)
 
964
        if delta_type == 'full' and not check_fileids:
 
965
            deltas = repository.get_deltas_for_revisions(revisions)
1020
966
            for rev, delta in zip(revs, deltas):
1021
967
                new_revs.append((rev[0], rev[1], delta))
1022
968
        else:
1023
 
            deltas = repository.get_revision_deltas(
1024
 
                revisions, specific_files=file_set)
 
969
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
1025
970
            for rev, delta in zip(revs, deltas):
1026
 
                if check_files:
 
971
                if check_fileids:
1027
972
                    if delta is None or not delta.has_changed():
1028
973
                        continue
1029
974
                    else:
1030
 
                        _update_files(delta, file_set, stop_on)
 
975
                        _update_fileids(delta, fileid_set, stop_on)
1031
976
                        if delta_type is None:
1032
977
                            delta = None
1033
978
                        elif delta_type == 'full':
1044
989
        yield new_revs
1045
990
 
1046
991
 
1047
 
def _update_files(delta, files, stop_on):
1048
 
    """Update the set of files to search based on file lifecycle events.
 
992
def _update_fileids(delta, fileids, stop_on):
 
993
    """Update the set of file-ids to search based on file lifecycle events.
1049
994
 
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
 
995
    :param fileids: a set of fileids to update
 
996
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
997
      fileids set once their add or remove entry is detected respectively
1053
998
    """
1054
999
    if stop_on == 'add':
1055
1000
        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]):])
 
1001
            if item[1] in fileids:
 
1002
                fileids.remove(item[1])
1067
1003
    elif stop_on == 'delete':
1068
1004
        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]):])
 
1005
            if item[1] in fileids:
 
1006
                fileids.remove(item[1])
1080
1007
 
1081
1008
 
1082
1009
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1123
1050
def _get_revision_limits(branch, start_revision, end_revision):
1124
1051
    """Get and check revision limits.
1125
1052
 
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
 
1053
    :param  branch: The branch containing the revisions.
 
1054
 
 
1055
    :param  start_revision: The first revision to be logged.
 
1056
            For backwards compatibility this may be a mainline integer revno,
 
1057
            but for merge revision support a RevisionInfo is expected.
 
1058
 
 
1059
    :param  end_revision: The last revision to be logged.
 
1060
            For backwards compatibility this may be a mainline integer revno,
 
1061
            but for merge revision support a RevisionInfo is expected.
1131
1062
 
1132
1063
    :return: (start_rev_id, end_rev_id) tuple.
1133
1064
    """
 
1065
    branch_revno, branch_rev_id = branch.last_revision_info()
1134
1066
    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:
 
1067
    if start_revision is None:
1142
1068
        start_revno = 1
 
1069
    else:
 
1070
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
1071
            start_rev_id = start_revision.rev_id
 
1072
            start_revno = start_revision.revno or 1
 
1073
        else:
 
1074
            branch.check_real_revno(start_revision)
 
1075
            start_revno = start_revision
 
1076
            start_rev_id = branch.get_rev_id(start_revno)
1143
1077
 
1144
1078
    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
 
1079
    if end_revision is None:
 
1080
        end_revno = branch_revno
 
1081
    else:
 
1082
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
1083
            end_rev_id = end_revision.rev_id
 
1084
            end_revno = end_revision.revno or branch_revno
 
1085
        else:
 
1086
            branch.check_real_revno(end_revision)
 
1087
            end_revno = end_revision
 
1088
            end_rev_id = branch.get_rev_id(end_revno)
1151
1089
 
1152
 
    if branch.last_revision() != _mod_revision.NULL_REVISION:
 
1090
    if branch_revno != 0:
1153
1091
        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."))
 
1092
            or end_rev_id == _mod_revision.NULL_REVISION):
 
1093
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1094
        if start_revno > end_revno:
 
1095
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1096
                                         "older than the end revision."))
1160
1097
    return (start_rev_id, end_rev_id)
1161
1098
 
1162
1099
 
1210
1147
            end_revno = end_revision
1211
1148
 
1212
1149
    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.'))
 
1150
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1151
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1215
1152
    if start_revno > end_revno:
1216
 
        raise errors.CommandError(gettext("Start revision must be older "
1217
 
                                             "than the end revision."))
 
1153
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1154
                                     "than the end revision."))
1218
1155
 
1219
1156
    if end_revno < start_revno:
1220
1157
        return None, None, None, None
1243
1180
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1244
1181
 
1245
1182
 
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.
 
1183
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1184
    include_merges=True):
 
1185
    r"""Return the list of revision ids which touch a given file id.
1249
1186
 
1250
1187
    The function filters view_revisions and returns a subset.
1251
 
    This includes the revisions which directly change the path,
 
1188
    This includes the revisions which directly change the file id,
1252
1189
    and the revisions which merge these changes. So if the
1253
1190
    revision graph is::
1254
1191
 
1271
1208
 
1272
1209
    :param branch: The branch where we can get text revision information.
1273
1210
 
1274
 
    :param path: Filter out revisions that do not touch path.
 
1211
    :param file_id: Filter out revisions that do not touch file_id.
1275
1212
 
1276
1213
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1277
1214
        tuples. This is the list of revisions which will be filtered. It is
1285
1222
    # Lookup all possible text keys to determine which ones actually modified
1286
1223
    # the file.
1287
1224
    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
1225
    get_parent_map = graph.get_parent_map
1291
1226
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1292
1227
    next_keys = None
1333
1268
    """Reverse revisions by depth.
1334
1269
 
1335
1270
    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
 
1271
    revision of that depth.  There may be no topological justification for this,
1337
1272
    but it looks much nicer.
1338
1273
    """
1339
1274
    # Add a fake revision at start so that we can always attach sub revisions
1446
1381
        """
1447
1382
        self.to_file = to_file
1448
1383
        # '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
 
1384
        # and should not try to decode/encode it to unicode to avoid bug #328007
1451
1385
        if to_exact_file is not None:
1452
1386
            self.to_exact_file = to_exact_file
1453
1387
        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
 
1388
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1389
            # for code that expects to get diffs to pass in the exact file
 
1390
            # stream
1457
1391
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1458
1392
        self.show_ids = show_ids
1459
1393
        self.show_timezone = show_timezone
1460
1394
        if delta_format is None:
1461
1395
            # Ensures backward compatibility
1462
 
            delta_format = 2  # long format
 
1396
            delta_format = 2 # long format
1463
1397
        self.delta_format = delta_format
1464
1398
        self.levels = levels
1465
1399
        self._show_advice = show_advice
1563
1497
        """
1564
1498
        lines = self._foreign_info_properties(revision)
1565
1499
        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)
 
1500
            lines.extend(self._format_properties(handler(revision)))
1571
1501
        return lines
1572
1502
 
1573
1503
    def _foreign_info_properties(self, rev):
1581
1511
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1582
1512
 
1583
1513
        # Imported foreign revision revision ids always contain :
1584
 
        if b":" not in rev.revision_id:
 
1514
        if not ":" in rev.revision_id:
1585
1515
            return []
1586
1516
 
1587
1517
        # Revision was once imported from a foreign repository
1601
1531
        return lines
1602
1532
 
1603
1533
    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')
 
1534
        for l in diff.rstrip().split('\n'):
 
1535
            to_file.write(indent + '%s\n' % (l,))
1607
1536
 
1608
1537
 
1609
1538
# Separator between revisions in long format
1632
1561
 
1633
1562
    def _date_string_original_timezone(self, rev):
1634
1563
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1635
 
                                                            rev.timezone or 0)
 
1564
            rev.timezone or 0)
1636
1565
 
1637
1566
    def log_revision(self, revision):
1638
1567
        """Log a revision, either merged or not."""
1640
1569
        lines = [_LONG_SEP]
1641
1570
        if revision.revno is not None:
1642
1571
            lines.append('revno: %s%s' % (revision.revno,
1643
 
                                          self.merge_marker(revision)))
 
1572
                self.merge_marker(revision)))
1644
1573
        if revision.tags:
1645
 
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1574
            lines.append('tags: %s' % (', '.join(revision.tags)))
1646
1575
        if self.show_ids or revision.revno is None:
1647
 
            lines.append('revision-id: %s' %
1648
 
                         (revision.rev.revision_id.decode('utf-8'),))
 
1576
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1649
1577
        if self.show_ids:
1650
1578
            for parent_id in revision.rev.parent_ids:
1651
 
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1579
                lines.append('parent: %s' % (parent_id,))
1652
1580
        lines.extend(self.custom_properties(revision.rev))
1653
1581
 
1654
1582
        committer = revision.rev.committer
1730
1658
        to_file = self.to_file
1731
1659
        tags = ''
1732
1660
        if revision.tags:
1733
 
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1661
            tags = ' {%s}' % (', '.join(revision.tags))
1734
1662
        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)
 
1663
                revision.revno or "", self.short_author(revision.rev),
 
1664
                format_date(revision.rev.timestamp,
 
1665
                            revision.rev.timezone or 0,
 
1666
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
1667
                            show_offset=False),
 
1668
                tags, self.merge_marker(revision)))
 
1669
        self.show_properties(revision.rev, indent+offset)
1743
1670
        if self.show_ids or revision.revno is None:
1744
1671
            to_file.write(indent + offset + 'revision-id:%s\n'
1745
 
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1672
                          % (revision.rev.revision_id,))
1746
1673
        if not revision.rev.message:
1747
1674
            to_file.write(indent + offset + '(no message)\n')
1748
1675
        else:
1754
1681
            # Use the standard status output to display changes
1755
1682
            from breezy.delta import report_delta
1756
1683
            report_delta(to_file, revision.delta,
1757
 
                         short_status=self.delta_format == 1,
 
1684
                         short_status=self.delta_format==1,
1758
1685
                         show_ids=self.show_ids, indent=indent + offset)
1759
1686
        if revision.diff is not None:
1760
1687
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1778
1705
    def truncate(self, str, max_len):
1779
1706
        if max_len is None or len(str) <= max_len:
1780
1707
            return str
1781
 
        return str[:max_len - 3] + '...'
 
1708
        return str[:max_len-3] + '...'
1782
1709
 
1783
1710
    def date_string(self, rev):
1784
1711
        return format_date(rev.timestamp, rev.timezone or 0,
1794
1721
    def log_revision(self, revision):
1795
1722
        indent = '  ' * revision.merge_depth
1796
1723
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1797
 
                                           self._max_chars, revision.tags, indent))
 
1724
            self._max_chars, revision.tags, indent))
1798
1725
        self.to_file.write('\n')
1799
1726
 
1800
1727
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1813
1740
            # show revno only when is not None
1814
1741
            out.append("%s:" % revno)
1815
1742
        if max_chars is not None:
1816
 
            out.append(self.truncate(
1817
 
                self.short_author(rev), (max_chars + 3) // 4))
 
1743
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1818
1744
        else:
1819
1745
            out.append(self.short_author(rev))
1820
1746
        out.append(self.date_string(rev))
1821
1747
        if len(rev.parent_ids) > 1:
1822
1748
            out.append('[merge]')
1823
1749
        if tags:
1824
 
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1750
            tag_str = '{%s}' % (', '.join(tags))
1825
1751
            out.append(tag_str)
1826
1752
        out.append(rev.get_summary())
1827
1753
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1847
1773
 
1848
1774
        if revision.delta is not None and revision.delta.has_changed():
1849
1775
            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]
 
1776
                path, = c[:1]
1854
1777
                to_file.write('\t* %s:\n' % (path,))
1855
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1778
            for c in revision.delta.renamed:
 
1779
                oldpath, newpath = c[:2]
1856
1780
                # 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]))
 
1781
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
1858
1782
            to_file.write('\n')
1859
1783
 
1860
1784
        if not revision.rev.message:
1913
1837
    try:
1914
1838
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1915
1839
    except KeyError:
1916
 
        raise errors.CommandError(
1917
 
            gettext("unknown log formatter: %r") % name)
 
1840
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1918
1841
 
1919
1842
 
1920
1843
def author_list_all(rev):
1956
1879
    """
1957
1880
    if to_file is None:
1958
1881
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1959
 
                                                            errors='replace')
 
1882
            errors='replace')
1960
1883
    lf = log_formatter(log_format,
1961
1884
                       show_ids=False,
1962
1885
                       to_file=to_file,
1968
1891
    for i in range(max(len(new_rh), len(old_rh))):
1969
1892
        if (len(new_rh) <= i
1970
1893
            or len(old_rh) <= i
1971
 
                or new_rh[i] != old_rh[i]):
 
1894
            or new_rh[i] != old_rh[i]):
1972
1895
            base_idx = i
1973
1896
            break
1974
1897
 
1975
1898
    if base_idx is None:
1976
1899
        to_file.write('Nothing seems to have changed\n')
1977
1900
        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
 
1901
    ## TODO: It might be nice to do something like show_log
 
1902
    ##       and show the merged entries. But since this is the
 
1903
    ##       removed revisions, it shouldn't be as important
1981
1904
    if base_idx < len(old_rh):
1982
 
        to_file.write('*' * 60)
 
1905
        to_file.write('*'*60)
1983
1906
        to_file.write('\nRemoved Revisions:\n')
1984
1907
        for i in range(base_idx, len(old_rh)):
1985
1908
            rev = branch.repository.get_revision(old_rh[i])
1986
 
            lr = LogRevision(rev, i + 1, 0, None)
 
1909
            lr = LogRevision(rev, i+1, 0, None)
1987
1910
            lf.log_revision(lr)
1988
 
        to_file.write('*' * 60)
 
1911
        to_file.write('*'*60)
1989
1912
        to_file.write('\n\n')
1990
1913
    if base_idx < len(new_rh):
1991
1914
        to_file.write('Added Revisions:\n')
1992
1915
        show_log(branch,
1993
1916
                 lf,
 
1917
                 None,
1994
1918
                 verbose=False,
1995
1919
                 direction='forward',
1996
 
                 start_revision=base_idx + 1,
1997
 
                 end_revision=len(new_rh))
 
1920
                 start_revision=base_idx+1,
 
1921
                 end_revision=len(new_rh),
 
1922
                 search=None)
1998
1923
 
1999
1924
 
2000
1925
def get_history_change(old_revision_id, new_revision_id, repository):
2066
1991
    log_format = log_formatter_registry.get_default(branch)
2067
1992
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
2068
1993
    if old_history != []:
2069
 
        output.write('*' * 60)
 
1994
        output.write('*'*60)
2070
1995
        output.write('\nRemoved Revisions:\n')
2071
1996
        show_flat_log(branch.repository, old_history, old_revno, lf)
2072
 
        output.write('*' * 60)
 
1997
        output.write('*'*60)
2073
1998
        output.write('\n\n')
2074
1999
    if new_history != []:
2075
2000
        output.write('Added Revisions:\n')
2076
2001
        start_revno = new_revno - len(new_history) + 1
2077
 
        show_log(branch, lf, verbose=False, direction='forward',
2078
 
                 start_revision=start_revno)
 
2002
        show_log(branch, lf, None, verbose=False, direction='forward',
 
2003
                 start_revision=start_revno,)
2079
2004
 
2080
2005
 
2081
2006
def show_flat_log(repository, history, last_revno, lf):
2086
2011
    :param last_revno: The revno of the last revision_id in the history.
2087
2012
    :param lf: The log formatter to use.
2088
2013
    """
 
2014
    start_revno = last_revno - len(history) + 1
2089
2015
    revisions = repository.get_revisions(history)
2090
2016
    for i, rev in enumerate(revisions):
2091
2017
        lr = LogRevision(rev, i + last_revno, 0, None)
2092
2018
        lf.log_revision(lr)
2093
2019
 
2094
2020
 
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.
 
2021
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2022
    """Find file-ids and kinds given a list of files and a revision range.
2097
2023
 
2098
2024
    We search for files at the end of the range. If not found there,
2099
2025
    we try the start of the range.
2102
2028
    :param file_list: the list of paths given on the command line;
2103
2029
      the first of these can be a branch location or a file path,
2104
2030
      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.
 
2031
    :param add_cleanup: When the branch returned is read locked,
 
2032
      an unlock call will be queued to the cleanup.
2107
2033
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2108
 
      info_list is a list of (relative_path, found, kind) tuples where
 
2034
      info_list is a list of (relative_path, file_id, kind) tuples where
2109
2035
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2110
2036
      branch will be read-locked.
2111
2037
    """
2112
2038
    from breezy.builtins import _get_revision_range
2113
2039
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2114
2040
        file_list[0])
2115
 
    exit_stack.enter_context(b.lock_read())
 
2041
    add_cleanup(b.lock_read().unlock)
2116
2042
    # XXX: It's damn messy converting a list of paths to relative paths when
2117
2043
    # those paths might be deleted ones, they might be on a case-insensitive
2118
2044
    # filesystem and/or they might be in silly locations (like another branch).
2127
2053
        relpaths = [path] + file_list[1:]
2128
2054
    info_list = []
2129
2055
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2130
 
                                                       "log")
 
2056
        "log")
2131
2057
    if relpaths in ([], [u'']):
2132
2058
        return b, [], start_rev_info, end_rev_info
2133
2059
    if start_rev_info is None and end_rev_info is None:
2135
2061
            tree = b.basis_tree()
2136
2062
        tree1 = None
2137
2063
        for fp in relpaths:
2138
 
            kind = _get_kind_for_file(tree, fp)
2139
 
            if not kind:
 
2064
            file_id = tree.path2id(fp)
 
2065
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2066
            if file_id is None:
2140
2067
                # go back to when time began
2141
2068
                if tree1 is None:
2142
2069
                    try:
2143
2070
                        rev1 = b.get_rev_id(1)
2144
2071
                    except errors.NoSuchRevision:
2145
2072
                        # No history at all
 
2073
                        file_id = None
2146
2074
                        kind = None
2147
2075
                    else:
2148
2076
                        tree1 = b.repository.revision_tree(rev1)
2149
2077
                if tree1:
2150
 
                    kind = _get_kind_for_file(tree1, fp)
2151
 
            info_list.append((fp, kind))
 
2078
                    file_id = tree1.path2id(fp)
 
2079
                    kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2080
            info_list.append((fp, file_id, kind))
2152
2081
 
2153
2082
    elif start_rev_info == end_rev_info:
2154
2083
        # One revision given - file must exist in it
2155
2084
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2156
2085
        for fp in relpaths:
2157
 
            kind = _get_kind_for_file(tree, fp)
2158
 
            info_list.append((fp, kind))
 
2086
            file_id = tree.path2id(fp)
 
2087
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2088
            info_list.append((fp, file_id, kind))
2159
2089
 
2160
2090
    else:
2161
2091
        # Revision range given. Get the file-id from the end tree.
2167
2097
            tree = b.repository.revision_tree(rev_id)
2168
2098
        tree1 = None
2169
2099
        for fp in relpaths:
2170
 
            kind = _get_kind_for_file(tree, fp)
2171
 
            if not kind:
 
2100
            file_id = tree.path2id(fp)
 
2101
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2102
            if file_id is None:
2172
2103
                if tree1 is None:
2173
2104
                    rev_id = start_rev_info.rev_id
2174
2105
                    if rev_id is None:
2176
2107
                        tree1 = b.repository.revision_tree(rev1)
2177
2108
                    else:
2178
2109
                        tree1 = b.repository.revision_tree(rev_id)
2179
 
                kind = _get_kind_for_file(tree1, fp)
2180
 
            info_list.append((fp, kind))
 
2110
                file_id = tree1.path2id(fp)
 
2111
                kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2112
            info_list.append((fp, file_id, kind))
2181
2113
    return b, info_list, start_rev_info, end_rev_info
2182
2114
 
2183
2115
 
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
 
2116
def _get_kind_for_file_id(tree, path, file_id):
 
2117
    """Return the kind of a file-id or None if it doesn't exist."""
 
2118
    if file_id is not None:
 
2119
        return tree.kind(path, file_id)
 
2120
    else:
 
2121
        return None
2191
2122
 
2192
2123
 
2193
2124
properties_handler_registry = registry.Registry()
2194
2125
 
2195
2126
# Use the properties handlers to print out bug information if available
2196
 
 
2197
 
 
2198
2127
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
 
2128
    if 'bugs' in revision.properties:
 
2129
        bug_lines = revision.properties['bugs'].split('\n')
 
2130
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2131
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2132
                          len(row) > 1 and row[1] == 'fixed']
2215
2133
 
 
2134
        if fixed_bug_urls:
 
2135
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2136
                    ' '.join(fixed_bug_urls)}
 
2137
    return {}
2216
2138
 
2217
2139
properties_handler_registry.register('bugs_properties_handler',
2218
2140
                                     _bugs_properties_handler)