/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-06-14 17:59:16 UTC
  • mto: This revision was merged to the branch mainline in revision 7065.
  • Revision ID: jelmer@jelmer.uk-20180614175916-a2e2xh5k533guq1x
Move breezy.plugins.git to breezy.git.

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
64
65
    controldir,
65
66
    diff,
66
67
    foreign,
67
 
    lazy_regex,
 
68
    repository as _mod_repository,
68
69
    revision as _mod_revision,
 
70
    tsort,
69
71
    )
70
72
from breezy.i18n import gettext, ngettext
71
73
""")
72
74
 
73
75
from . import (
74
76
    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 .sixish import (
 
89
    BytesIO,
 
90
    range,
 
91
    zip,
90
92
    )
 
93
from .tree import find_previous_path
91
94
 
92
95
 
93
96
def find_touching_revisions(repository, last_revision, last_tree, last_path):
94
 
    """Yield a description of revisions which affect the file.
 
97
    """Yield a description of revisions which affect the file_id.
95
98
 
96
99
    Each returned element is (revno, revision_id, description)
97
100
 
107
110
    revno = len(history)
108
111
    for revision_id in history:
109
112
        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)
 
113
        this_path = find_previous_path(last_tree, this_tree, last_path)
112
114
 
113
115
        # now we know how it was last time, and how it is in this revision.
114
116
        # are those two states effectively the same or not?
135
137
 
136
138
def show_log(branch,
137
139
             lf,
 
140
             specific_fileid=None,
138
141
             verbose=False,
139
142
             direction='reverse',
140
143
             start_revision=None,
141
144
             end_revision=None,
 
145
             search=None,
142
146
             limit=None,
143
147
             show_diff=False,
144
148
             match=None):
151
155
 
152
156
    :param lf: The LogFormatter object showing the output.
153
157
 
 
158
    :param specific_fileid: If not None, list only the commits affecting the
 
159
        specified file, rather than all commits.
 
160
 
154
161
    :param verbose: If True show added/changed/deleted/renamed files.
155
162
 
156
163
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
160
167
 
161
168
    :param end_revision: If not None, only show revisions <= end_revision
162
169
 
 
170
    :param search: If not None, only show revisions with matching commit
 
171
        messages
 
172
 
163
173
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
164
174
        if None or 0.
165
175
 
168
178
    :param match: Dictionary of search lists to use when matching revision
169
179
      properties.
170
180
    """
 
181
    # Convert old-style parameters to new-style parameters
 
182
    if specific_fileid is not None:
 
183
        file_ids = [specific_fileid]
 
184
    else:
 
185
        file_ids = None
171
186
    if verbose:
172
 
        delta_type = 'full'
 
187
        if file_ids:
 
188
            delta_type = 'partial'
 
189
        else:
 
190
            delta_type = 'full'
173
191
    else:
174
192
        delta_type = None
175
193
    if show_diff:
176
 
        diff_type = 'full'
 
194
        if file_ids:
 
195
            diff_type = 'partial'
 
196
        else:
 
197
            diff_type = 'full'
177
198
    else:
178
199
        diff_type = None
179
200
 
180
201
    if isinstance(start_revision, int):
181
202
        try:
182
203
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
183
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
204
        except errors.NoSuchRevision:
184
205
            raise errors.InvalidRevisionNumber(start_revision)
185
206
 
186
207
    if isinstance(end_revision, int):
187
208
        try:
188
209
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
189
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
210
        except errors.NoSuchRevision:
190
211
            raise errors.InvalidRevisionNumber(end_revision)
191
212
 
192
213
    if end_revision is not None and end_revision.revno == 0:
193
214
        raise errors.InvalidRevisionNumber(end_revision.revno)
194
215
 
195
216
    # Build the request and execute it
196
 
    rqst = make_log_request_dict(
197
 
        direction=direction,
 
217
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
198
218
        start_revision=start_revision, end_revision=end_revision,
199
 
        limit=limit, delta_type=delta_type, diff_type=diff_type)
 
219
        limit=limit, message_search=search,
 
220
        delta_type=delta_type, diff_type=diff_type)
200
221
    Logger(branch, rqst).show(lf)
201
222
 
202
223
 
211
232
    }
212
233
 
213
234
 
214
 
def make_log_request_dict(direction='reverse', specific_files=None,
 
235
def make_log_request_dict(direction='reverse', specific_fileids=None,
215
236
                          start_revision=None, end_revision=None, limit=None,
216
237
                          message_search=None, levels=None, generate_tags=True,
217
238
                          delta_type=None,
228
249
    :param direction: 'reverse' (default) is latest to earliest;
229
250
      'forward' is earliest to latest.
230
251
 
231
 
    :param specific_files: If not None, only include revisions
 
252
    :param specific_fileids: If not None, only include revisions
232
253
      affecting the specified files, rather than all revisions.
233
254
 
234
255
    :param start_revision: If not None, only generate
251
272
`
252
273
    :param delta_type: Either 'full', 'partial' or None.
253
274
      'full' means generate the complete delta - adds/deletes/modifies/etc;
254
 
      'partial' means filter the delta using specific_files;
 
275
      'partial' means filter the delta using specific_fileids;
255
276
      None means do not generate any delta.
256
277
 
257
278
    :param diff_type: Either 'full', 'partial' or None.
258
279
      'full' means generate the complete diff - adds/deletes/modifies/etc;
259
 
      'partial' means filter the diff using specific_files;
 
280
      'partial' means filter the diff using specific_fileids;
260
281
      None means do not generate any diff.
261
282
 
262
283
    :param _match_using_deltas: a private parameter controlling the
263
 
      algorithm used for matching specific_files. This parameter
 
284
      algorithm used for matching specific_fileids. This parameter
264
285
      may be removed in the future so breezy client code should NOT
265
286
      use it.
266
287
 
285
306
            else:
286
307
                match['message'] = [message_search]
287
308
        else:
288
 
            match = {'message': [message_search]}
 
309
            match={ 'message': [message_search] }
289
310
    return {
290
311
        'direction': direction,
291
 
        'specific_files': specific_files,
 
312
        'specific_fileids': specific_fileids,
292
313
        'start_revision': start_revision,
293
314
        'end_revision': end_revision,
294
315
        'limit': limit,
400
421
            for lr in generator.iter_log_revisions():
401
422
                lf.log_revision(lr)
402
423
        except errors.GhostRevisionUnusableHere:
403
 
            raise errors.CommandError(
404
 
                gettext('Further revision history missing.'))
 
424
            raise errors.BzrCommandError(
 
425
                    gettext('Further revision history missing.'))
405
426
        lf.show_advice()
406
427
 
407
428
    def _generator_factory(self, branch, rqst):
409
430
 
410
431
        Subclasses may wish to override this.
411
432
        """
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()
 
433
        return _DefaultLogGenerator(branch, rqst)
479
434
 
480
435
 
481
436
class _StartNotLinearAncestor(Exception):
485
440
class _DefaultLogGenerator(LogGenerator):
486
441
    """The default generator of log revisions."""
487
442
 
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):
 
443
    def __init__(self, branch, rqst):
495
444
        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():
 
445
        self.rqst = rqst
 
446
        if rqst.get('generate_tags') and branch.supports_tags():
510
447
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
511
448
        else:
512
449
            self.rev_tag_dict = {}
516
453
 
517
454
        :return: An iterator yielding LogRevision objects.
518
455
        """
 
456
        rqst = self.rqst
 
457
        levels = rqst.get('levels')
 
458
        limit = rqst.get('limit')
 
459
        diff_type = rqst.get('diff_type')
 
460
        show_signature = rqst.get('signature')
 
461
        omit_merges = rqst.get('omit_merges')
519
462
        log_count = 0
520
463
        revision_iterator = self._create_log_revision_iterator()
521
464
        for revs in revision_iterator:
522
465
            for (rev_id, revno, merge_depth), rev, delta in revs:
523
466
                # 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):
 
467
                if levels != 0 and merge_depth >= levels:
526
468
                    continue
527
 
                if self.omit_merges and len(rev.parent_ids) > 1:
 
469
                if omit_merges and len(rev.parent_ids) > 1:
528
470
                    continue
529
471
                if rev is None:
530
472
                    raise errors.GhostRevisionUnusableHere(rev_id)
531
 
                if self.diff_type is None:
 
473
                if diff_type is None:
532
474
                    diff = None
533
475
                else:
534
 
                    diff = _format_diff(
535
 
                        self.branch, rev, self.diff_type,
536
 
                        self.specific_files)
537
 
                if self.show_signature:
 
476
                    diff = self._format_diff(rev, rev_id, diff_type)
 
477
                if show_signature:
538
478
                    signature = format_signature_validity(rev_id, self.branch)
539
479
                else:
540
480
                    signature = None
541
 
                yield LogRevision(
542
 
                    rev, revno, merge_depth, delta,
 
481
                yield LogRevision(rev, revno, merge_depth, delta,
543
482
                    self.rev_tag_dict.get(rev_id), diff, signature)
544
 
                if self.limit:
 
483
                if limit:
545
484
                    log_count += 1
546
 
                    if log_count >= self.limit:
 
485
                    if log_count >= limit:
547
486
                        return
548
487
 
 
488
    def _format_diff(self, rev, rev_id, diff_type):
 
489
        repo = self.branch.repository
 
490
        if len(rev.parent_ids) == 0:
 
491
            ancestor_id = _mod_revision.NULL_REVISION
 
492
        else:
 
493
            ancestor_id = rev.parent_ids[0]
 
494
        tree_1 = repo.revision_tree(ancestor_id)
 
495
        tree_2 = repo.revision_tree(rev_id)
 
496
        file_ids = self.rqst.get('specific_fileids')
 
497
        if diff_type == 'partial' and file_ids is not None:
 
498
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
499
        else:
 
500
            specific_files = None
 
501
        s = BytesIO()
 
502
        path_encoding = get_diff_header_encoding()
 
503
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
504
            new_label='', path_encoding=path_encoding)
 
505
        return s.getvalue()
 
506
 
549
507
    def _create_log_revision_iterator(self):
550
508
        """Create a revision iterator for log.
551
509
 
552
510
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
553
511
            delta).
554
512
        """
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)
 
513
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
514
            self.branch, self.rqst.get('start_revision'),
 
515
            self.rqst.get('end_revision'))
 
516
        if self.rqst.get('_match_using_deltas'):
 
517
            return self._log_revision_iterator_using_delta_matching()
568
518
        else:
569
519
            # We're using the per-file-graph algorithm. This scales really
570
520
            # well but only makes sense if there is a single file and it's
571
521
            # not a directory
572
 
            file_count = len(self.specific_files)
 
522
            file_count = len(self.rqst.get('specific_fileids'))
573
523
            if file_count != 1:
574
 
                raise errors.BzrError(
575
 
                    "illegal LogRequest: must match-using-deltas "
 
524
                raise BzrError("illegal LogRequest: must match-using-deltas "
576
525
                    "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
 
                )
 
526
            return self._log_revision_iterator_using_per_file_graph()
 
527
 
 
528
    def _log_revision_iterator_using_delta_matching(self):
 
529
        # Get the base revisions, filtering by the revision range
 
530
        rqst = self.rqst
 
531
        generate_merge_revisions = rqst.get('levels') != 1
 
532
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
533
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
534
        view_revisions = _calc_view_revisions(
 
535
            self.branch, self.start_rev_id, self.end_rev_id,
 
536
            rqst.get('direction'),
 
537
            generate_merge_revisions=generate_merge_revisions,
 
538
            delayed_graph_generation=delayed_graph_generation,
 
539
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
540
 
 
541
        # Apply the other filters
 
542
        return make_log_rev_iterator(self.branch, view_revisions,
 
543
            rqst.get('delta_type'), rqst.get('match'),
 
544
            file_ids=rqst.get('specific_fileids'),
 
545
            direction=rqst.get('direction'))
 
546
 
 
547
    def _log_revision_iterator_using_per_file_graph(self):
 
548
        # Get the base revisions, filtering by the revision range.
 
549
        # Note that we always generate the merge revisions because
 
550
        # filter_revisions_touching_file_id() requires them ...
 
551
        rqst = self.rqst
 
552
        view_revisions = _calc_view_revisions(
 
553
            self.branch, self.start_rev_id, self.end_rev_id,
 
554
            rqst.get('direction'), generate_merge_revisions=True,
 
555
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
556
        if not isinstance(view_revisions, list):
 
557
            view_revisions = list(view_revisions)
 
558
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
559
            rqst.get('specific_fileids')[0], view_revisions,
 
560
            include_merges=rqst.get('levels') != 1)
 
561
        return make_log_rev_iterator(self.branch, view_revisions,
 
562
            rqst.get('delta_type'), rqst.get('match'))
587
563
 
588
564
 
589
565
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
597
573
             a list of the same tuples.
598
574
    """
599
575
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
 
        raise errors.CommandError(gettext(
 
576
        raise errors.BzrCommandError(gettext(
601
577
            '--exclude-common-ancestry requires two different revisions'))
602
578
    if direction not in ('reverse', 'forward'):
603
579
        raise ValueError(gettext('invalid direction %r') % direction)
609
585
        and (not generate_merge_revisions
610
586
             or not _has_merges(branch, end_rev_id))):
611
587
        # 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())
 
588
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
589
                                       branch.revno())
614
590
    if not generate_merge_revisions:
615
591
        try:
616
592
            # If we only want to see linear revisions, we can iterate ...
621
597
            # ancestor of the end limit, check it before outputting anything
622
598
            if (direction == 'forward'
623
599
                or (start_rev_id and not _is_obvious_ancestor(
624
 
                    branch, start_rev_id, end_rev_id))):
625
 
                iter_revs = list(iter_revs)
 
600
                        branch, start_rev_id, end_rev_id))):
 
601
                    iter_revs = list(iter_revs)
626
602
            if direction == 'forward':
627
603
                iter_revs = reversed(iter_revs)
628
604
            return iter_revs
660
636
    initial_revisions = []
661
637
    if delayed_graph_generation:
662
638
        try:
663
 
            for rev_id, revno, depth in _linear_view_revisions(
664
 
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
639
            for rev_id, revno, depth in  _linear_view_revisions(
 
640
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
665
641
                if _has_merges(branch, rev_id):
666
642
                    # The end_rev_id can be nested down somewhere. We need an
667
643
                    # explicit ancestry check. There is an ambiguity here as we
674
650
                    # -- vila 20100319
675
651
                    graph = branch.repository.get_graph()
676
652
                    if (start_rev_id is not None
677
 
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
653
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
678
654
                        raise _StartNotLinearAncestor()
679
655
                    # Since we collected the revisions so far, we need to
680
656
                    # adjust end_rev_id.
688
664
        except _StartNotLinearAncestor:
689
665
            # A merge was never detected so the lower revision limit can't
690
666
            # be nested down somewhere
691
 
            raise errors.CommandError(gettext('Start revision not found in'
692
 
                                                 ' history of end revision.'))
 
667
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
668
                ' history of end revision.'))
693
669
 
694
670
    # We exit the loop above because we encounter a revision with merges, from
695
671
    # this revision, we need to switch to _graph_view_revisions.
700
676
    # make forward the exact opposite display, but showing the merge revisions
701
677
    # indented at the end seems slightly nicer in that case.
702
678
    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))
 
679
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
680
                              rebase_initial_depths=(direction == 'reverse'),
 
681
                              exclude_common_ancestry=exclude_common_ancestry))
707
682
    return view_revisions
708
683
 
709
684
 
741
716
            # both on mainline
742
717
            return start_dotted[0] <= end_dotted[0]
743
718
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
744
 
              start_dotted[0:1] == end_dotted[0:1]):
 
719
            start_dotted[0:1] == end_dotted[0:1]):
745
720
            # both on same development line
746
721
            return start_dotted[2] <= end_dotted[2]
747
722
        else:
768
743
    repo = branch.repository
769
744
    graph = repo.get_graph()
770
745
    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:
 
746
        try:
 
747
            br_revno, br_rev_id = branch.last_revision_info()
 
748
        except errors.GhostRevisionsHaveNoRevno:
781
749
            br_rev_id = branch.last_revision()
782
750
            cur_revno = None
783
 
 
 
751
        else:
 
752
            cur_revno = br_revno
784
753
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
 
                                                  (_mod_revision.NULL_REVISION,))
 
754
            (_mod_revision.NULL_REVISION,))
786
755
        while True:
787
756
            try:
788
757
                revision_id = next(graph_iter)
790
759
                # Oops, a ghost.
791
760
                yield e.revision_id, None, None
792
761
                break
793
 
            except StopIteration:
794
 
                break
795
762
            else:
796
763
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
797
764
                if cur_revno is not None:
802
769
            end_rev_id = br_rev_id
803
770
        found_start = start_rev_id is None
804
771
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
805
 
                                                  (_mod_revision.NULL_REVISION,))
 
772
            (_mod_revision.NULL_REVISION,))
806
773
        while True:
807
774
            try:
808
775
                revision_id = next(graph_iter)
875
842
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
876
843
        min_depth = min([d for r, n, d in view_revisions])
877
844
        if min_depth != 0:
878
 
            view_revisions = [(r, n, d - min_depth)
879
 
                              for r, n, d in view_revisions]
 
845
            view_revisions = [(r, n, d-min_depth) for r, n, d in view_revisions]
880
846
    return view_revisions
881
847
 
882
848
 
883
849
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
884
 
                          files=None, direction='reverse'):
 
850
        file_ids=None, direction='reverse'):
885
851
    """Create a revision iterator for log.
886
852
 
887
853
    :param branch: The branch being logged.
889
855
    :param generate_delta: Whether to generate a delta for each revision.
890
856
      Permitted values are None, 'full' and 'partial'.
891
857
    :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.
 
858
    :param file_ids: If non empty, only revisions matching one or more of
 
859
      the file-ids are to be kept.
894
860
    :param direction: the direction in which view_revisions is sorted
895
861
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
896
862
        delta).
911
877
        # It would be nicer if log adapters were first class objects
912
878
        # with custom parameters. This will do for now. IGC 20090127
913
879
        if adapter == _make_delta_filter:
914
 
            log_rev_iterator = adapter(
915
 
                branch, generate_delta, search, log_rev_iterator, files,
916
 
                direction)
 
880
            log_rev_iterator = adapter(branch, generate_delta,
 
881
                search, log_rev_iterator, file_ids, direction)
917
882
        else:
918
 
            log_rev_iterator = adapter(
919
 
                branch, generate_delta, search, log_rev_iterator)
 
883
            log_rev_iterator = adapter(branch, generate_delta,
 
884
                search, log_rev_iterator)
920
885
    return log_rev_iterator
921
886
 
922
887
 
936
901
    """
937
902
    if not match:
938
903
        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])
 
904
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
941
905
                for k, v in match.items()]
942
906
    return _filter_re(searchRE, log_rev_iterator)
943
907
 
948
912
        if new_revs:
949
913
            yield new_revs
950
914
 
951
 
 
952
915
def _match_filter(searchRE, rev):
953
916
    strings = {
954
 
        'message': (rev.message,),
955
 
        'committer': (rev.committer,),
956
 
        'author': (rev.get_apparent_authors()),
957
 
        'bugs': list(rev.iter_bugs())
958
 
        }
 
917
               'message': (rev.message,),
 
918
               'committer': (rev.committer,),
 
919
               'author': (rev.get_apparent_authors()),
 
920
               'bugs': list(rev.iter_bugs())
 
921
               }
959
922
    strings[''] = [item for inner_list in strings.values()
960
923
                   for item in inner_list]
961
 
    for k, v in searchRE:
 
924
    for (k, v) in searchRE:
962
925
        if k in strings and not _match_any_filter(strings[k], v):
963
926
            return False
964
927
    return True
965
928
 
966
 
 
967
929
def _match_any_filter(strings, res):
968
 
    return any(r.search(s) for r in res for s in strings)
969
 
 
 
930
    return any(re.search(s) for re in res for s in strings)
970
931
 
971
932
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
 
                       files=None, direction='reverse'):
 
933
    fileids=None, direction='reverse'):
973
934
    """Add revision deltas to a log iterator if needed.
974
935
 
975
936
    :param branch: The branch being logged.
978
939
    :param search: A user text search string.
979
940
    :param log_rev_iterator: An input iterator containing all revisions that
980
941
        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.
 
942
    :param fileids: If non empty, only revisions matching one or more of
 
943
      the file-ids are to be kept.
983
944
    :param direction: the direction in which view_revisions is sorted
984
945
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
985
946
        delta).
986
947
    """
987
 
    if not generate_delta and not files:
 
948
    if not generate_delta and not fileids:
988
949
        return log_rev_iterator
989
950
    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):
 
951
        generate_delta, fileids, direction)
 
952
 
 
953
 
 
954
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
 
955
    direction):
995
956
    """Create deltas for each batch of revisions in log_rev_iterator.
996
957
 
997
958
    If we're only generating deltas for the sake of filtering against
998
 
    files, we stop generating deltas once all files reach the
 
959
    file-ids, we stop generating deltas once all file-ids reach the
999
960
    appropriate life-cycle point. If we're receiving data newest to
1000
961
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1001
962
    """
1002
 
    check_files = files is not None and len(files) > 0
1003
 
    if check_files:
1004
 
        file_set = set(files)
 
963
    check_fileids = fileids is not None and len(fileids) > 0
 
964
    if check_fileids:
 
965
        fileid_set = set(fileids)
1005
966
        if direction == 'reverse':
1006
967
            stop_on = 'add'
1007
968
        else:
1008
969
            stop_on = 'remove'
1009
970
    else:
1010
 
        file_set = None
 
971
        fileid_set = None
1011
972
    for revs in log_rev_iterator:
1012
 
        # If we were matching against files and we've run out,
 
973
        # If we were matching against fileids and we've run out,
1013
974
        # there's nothing left to do
1014
 
        if check_files and not file_set:
 
975
        if check_fileids and not fileid_set:
1015
976
            return
1016
977
        revisions = [rev[1] for rev in revs]
1017
978
        new_revs = []
1018
 
        if delta_type == 'full' and not check_files:
1019
 
            deltas = repository.get_revision_deltas(revisions)
 
979
        if delta_type == 'full' and not check_fileids:
 
980
            deltas = repository.get_deltas_for_revisions(revisions)
1020
981
            for rev, delta in zip(revs, deltas):
1021
982
                new_revs.append((rev[0], rev[1], delta))
1022
983
        else:
1023
 
            deltas = repository.get_revision_deltas(
1024
 
                revisions, specific_files=file_set)
 
984
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
1025
985
            for rev, delta in zip(revs, deltas):
1026
 
                if check_files:
 
986
                if check_fileids:
1027
987
                    if delta is None or not delta.has_changed():
1028
988
                        continue
1029
989
                    else:
1030
 
                        _update_files(delta, file_set, stop_on)
 
990
                        _update_fileids(delta, fileid_set, stop_on)
1031
991
                        if delta_type is None:
1032
992
                            delta = None
1033
993
                        elif delta_type == 'full':
1044
1004
        yield new_revs
1045
1005
 
1046
1006
 
1047
 
def _update_files(delta, files, stop_on):
1048
 
    """Update the set of files to search based on file lifecycle events.
 
1007
def _update_fileids(delta, fileids, stop_on):
 
1008
    """Update the set of file-ids to search based on file lifecycle events.
1049
1009
 
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
 
1010
    :param fileids: a set of fileids to update
 
1011
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
1012
      fileids set once their add or remove entry is detected respectively
1053
1013
    """
1054
1014
    if stop_on == 'add':
1055
1015
        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]):])
 
1016
            if item[1] in fileids:
 
1017
                fileids.remove(item[1])
1067
1018
    elif stop_on == 'delete':
1068
1019
        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]):])
 
1020
            if item[1] in fileids:
 
1021
                fileids.remove(item[1])
1080
1022
 
1081
1023
 
1082
1024
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1123
1065
def _get_revision_limits(branch, start_revision, end_revision):
1124
1066
    """Get and check revision limits.
1125
1067
 
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
 
1068
    :param  branch: The branch containing the revisions.
 
1069
 
 
1070
    :param  start_revision: The first revision to be logged.
 
1071
            but for merge revision support a RevisionInfo is expected.
 
1072
 
 
1073
    :param  end_revision: The last revision to be logged.
 
1074
            For backwards compatibility this may be a mainline integer revno,
 
1075
            but for merge revision support a RevisionInfo is expected.
1131
1076
 
1132
1077
    :return: (start_rev_id, end_rev_id) tuple.
1133
1078
    """
1148
1093
            raise TypeError(start_revision)
1149
1094
        end_rev_id = end_revision.rev_id
1150
1095
        end_revno = end_revision.revno
 
1096
    if end_revno is None:
 
1097
        try:
 
1098
            end_revno = branch.revno()
 
1099
        except errors.GhostRevisionsHaveNoRevno:
 
1100
            end_revno = None
1151
1101
 
1152
1102
    if branch.last_revision() != _mod_revision.NULL_REVISION:
1153
1103
        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.'))
 
1104
            or end_rev_id == _mod_revision.NULL_REVISION):
 
1105
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1157
1106
        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."))
 
1107
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1108
                                         "older than the end revision."))
1160
1109
    return (start_rev_id, end_rev_id)
1161
1110
 
1162
1111
 
1210
1159
            end_revno = end_revision
1211
1160
 
1212
1161
    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.'))
 
1162
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1163
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1215
1164
    if start_revno > end_revno:
1216
 
        raise errors.CommandError(gettext("Start revision must be older "
1217
 
                                             "than the end revision."))
 
1165
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1166
                                     "than the end revision."))
1218
1167
 
1219
1168
    if end_revno < start_revno:
1220
1169
        return None, None, None, None
1243
1192
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1244
1193
 
1245
1194
 
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.
 
1195
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1196
    include_merges=True):
 
1197
    r"""Return the list of revision ids which touch a given file id.
1249
1198
 
1250
1199
    The function filters view_revisions and returns a subset.
1251
 
    This includes the revisions which directly change the path,
 
1200
    This includes the revisions which directly change the file id,
1252
1201
    and the revisions which merge these changes. So if the
1253
1202
    revision graph is::
1254
1203
 
1271
1220
 
1272
1221
    :param branch: The branch where we can get text revision information.
1273
1222
 
1274
 
    :param path: Filter out revisions that do not touch path.
 
1223
    :param file_id: Filter out revisions that do not touch file_id.
1275
1224
 
1276
1225
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1277
1226
        tuples. This is the list of revisions which will be filtered. It is
1285
1234
    # Lookup all possible text keys to determine which ones actually modified
1286
1235
    # the file.
1287
1236
    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
1237
    get_parent_map = graph.get_parent_map
1291
1238
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1292
1239
    next_keys = None
1333
1280
    """Reverse revisions by depth.
1334
1281
 
1335
1282
    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
 
1283
    revision of that depth.  There may be no topological justification for this,
1337
1284
    but it looks much nicer.
1338
1285
    """
1339
1286
    # Add a fake revision at start so that we can always attach sub revisions
1446
1393
        """
1447
1394
        self.to_file = to_file
1448
1395
        # '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
 
1396
        # and should not try to decode/encode it to unicode to avoid bug #328007
1451
1397
        if to_exact_file is not None:
1452
1398
            self.to_exact_file = to_exact_file
1453
1399
        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
 
1400
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1401
            # for code that expects to get diffs to pass in the exact file
 
1402
            # stream
1457
1403
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1458
1404
        self.show_ids = show_ids
1459
1405
        self.show_timezone = show_timezone
1460
1406
        if delta_format is None:
1461
1407
            # Ensures backward compatibility
1462
 
            delta_format = 2  # long format
 
1408
            delta_format = 2 # long format
1463
1409
        self.delta_format = delta_format
1464
1410
        self.levels = levels
1465
1411
        self._show_advice = show_advice
1563
1509
        """
1564
1510
        lines = self._foreign_info_properties(revision)
1565
1511
        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)
 
1512
            lines.extend(self._format_properties(handler(revision)))
1571
1513
        return lines
1572
1514
 
1573
1515
    def _foreign_info_properties(self, rev):
1581
1523
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1582
1524
 
1583
1525
        # Imported foreign revision revision ids always contain :
1584
 
        if b":" not in rev.revision_id:
 
1526
        if not ":" in rev.revision_id:
1585
1527
            return []
1586
1528
 
1587
1529
        # Revision was once imported from a foreign repository
1601
1543
        return lines
1602
1544
 
1603
1545
    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')
 
1546
        for l in diff.rstrip().split('\n'):
 
1547
            to_file.write(indent + '%s\n' % (l,))
1607
1548
 
1608
1549
 
1609
1550
# Separator between revisions in long format
1632
1573
 
1633
1574
    def _date_string_original_timezone(self, rev):
1634
1575
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1635
 
                                                            rev.timezone or 0)
 
1576
            rev.timezone or 0)
1636
1577
 
1637
1578
    def log_revision(self, revision):
1638
1579
        """Log a revision, either merged or not."""
1640
1581
        lines = [_LONG_SEP]
1641
1582
        if revision.revno is not None:
1642
1583
            lines.append('revno: %s%s' % (revision.revno,
1643
 
                                          self.merge_marker(revision)))
 
1584
                self.merge_marker(revision)))
1644
1585
        if revision.tags:
1645
 
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1586
            lines.append('tags: %s' % (', '.join(revision.tags)))
1646
1587
        if self.show_ids or revision.revno is None:
1647
 
            lines.append('revision-id: %s' %
1648
 
                         (revision.rev.revision_id.decode('utf-8'),))
 
1588
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1649
1589
        if self.show_ids:
1650
1590
            for parent_id in revision.rev.parent_ids:
1651
 
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1591
                lines.append('parent: %s' % (parent_id,))
1652
1592
        lines.extend(self.custom_properties(revision.rev))
1653
1593
 
1654
1594
        committer = revision.rev.committer
1730
1670
        to_file = self.to_file
1731
1671
        tags = ''
1732
1672
        if revision.tags:
1733
 
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1673
            tags = ' {%s}' % (', '.join(revision.tags))
1734
1674
        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)
 
1675
                revision.revno or "", self.short_author(revision.rev),
 
1676
                format_date(revision.rev.timestamp,
 
1677
                            revision.rev.timezone or 0,
 
1678
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
1679
                            show_offset=False),
 
1680
                tags, self.merge_marker(revision)))
 
1681
        self.show_properties(revision.rev, indent+offset)
1743
1682
        if self.show_ids or revision.revno is None:
1744
1683
            to_file.write(indent + offset + 'revision-id:%s\n'
1745
 
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1684
                          % (revision.rev.revision_id,))
1746
1685
        if not revision.rev.message:
1747
1686
            to_file.write(indent + offset + '(no message)\n')
1748
1687
        else:
1754
1693
            # Use the standard status output to display changes
1755
1694
            from breezy.delta import report_delta
1756
1695
            report_delta(to_file, revision.delta,
1757
 
                         short_status=self.delta_format == 1,
 
1696
                         short_status=self.delta_format==1,
1758
1697
                         show_ids=self.show_ids, indent=indent + offset)
1759
1698
        if revision.diff is not None:
1760
1699
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1778
1717
    def truncate(self, str, max_len):
1779
1718
        if max_len is None or len(str) <= max_len:
1780
1719
            return str
1781
 
        return str[:max_len - 3] + '...'
 
1720
        return str[:max_len-3] + '...'
1782
1721
 
1783
1722
    def date_string(self, rev):
1784
1723
        return format_date(rev.timestamp, rev.timezone or 0,
1794
1733
    def log_revision(self, revision):
1795
1734
        indent = '  ' * revision.merge_depth
1796
1735
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1797
 
                                           self._max_chars, revision.tags, indent))
 
1736
            self._max_chars, revision.tags, indent))
1798
1737
        self.to_file.write('\n')
1799
1738
 
1800
1739
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1813
1752
            # show revno only when is not None
1814
1753
            out.append("%s:" % revno)
1815
1754
        if max_chars is not None:
1816
 
            out.append(self.truncate(
1817
 
                self.short_author(rev), (max_chars + 3) // 4))
 
1755
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1818
1756
        else:
1819
1757
            out.append(self.short_author(rev))
1820
1758
        out.append(self.date_string(rev))
1821
1759
        if len(rev.parent_ids) > 1:
1822
1760
            out.append('[merge]')
1823
1761
        if tags:
1824
 
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1762
            tag_str = '{%s}' % (', '.join(tags))
1825
1763
            out.append(tag_str)
1826
1764
        out.append(rev.get_summary())
1827
1765
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1847
1785
 
1848
1786
        if revision.delta is not None and revision.delta.has_changed():
1849
1787
            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]
 
1788
                path, = c[:1]
1854
1789
                to_file.write('\t* %s:\n' % (path,))
1855
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1790
            for c in revision.delta.renamed:
 
1791
                oldpath, newpath = c[:2]
1856
1792
                # 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]))
 
1793
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
1858
1794
            to_file.write('\n')
1859
1795
 
1860
1796
        if not revision.rev.message:
1913
1849
    try:
1914
1850
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1915
1851
    except KeyError:
1916
 
        raise errors.CommandError(
1917
 
            gettext("unknown log formatter: %r") % name)
 
1852
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1918
1853
 
1919
1854
 
1920
1855
def author_list_all(rev):
1956
1891
    """
1957
1892
    if to_file is None:
1958
1893
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1959
 
                                                            errors='replace')
 
1894
            errors='replace')
1960
1895
    lf = log_formatter(log_format,
1961
1896
                       show_ids=False,
1962
1897
                       to_file=to_file,
1968
1903
    for i in range(max(len(new_rh), len(old_rh))):
1969
1904
        if (len(new_rh) <= i
1970
1905
            or len(old_rh) <= i
1971
 
                or new_rh[i] != old_rh[i]):
 
1906
            or new_rh[i] != old_rh[i]):
1972
1907
            base_idx = i
1973
1908
            break
1974
1909
 
1975
1910
    if base_idx is None:
1976
1911
        to_file.write('Nothing seems to have changed\n')
1977
1912
        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
 
1913
    ## TODO: It might be nice to do something like show_log
 
1914
    ##       and show the merged entries. But since this is the
 
1915
    ##       removed revisions, it shouldn't be as important
1981
1916
    if base_idx < len(old_rh):
1982
 
        to_file.write('*' * 60)
 
1917
        to_file.write('*'*60)
1983
1918
        to_file.write('\nRemoved Revisions:\n')
1984
1919
        for i in range(base_idx, len(old_rh)):
1985
1920
            rev = branch.repository.get_revision(old_rh[i])
1986
 
            lr = LogRevision(rev, i + 1, 0, None)
 
1921
            lr = LogRevision(rev, i+1, 0, None)
1987
1922
            lf.log_revision(lr)
1988
 
        to_file.write('*' * 60)
 
1923
        to_file.write('*'*60)
1989
1924
        to_file.write('\n\n')
1990
1925
    if base_idx < len(new_rh):
1991
1926
        to_file.write('Added Revisions:\n')
1992
1927
        show_log(branch,
1993
1928
                 lf,
 
1929
                 None,
1994
1930
                 verbose=False,
1995
1931
                 direction='forward',
1996
 
                 start_revision=base_idx + 1,
1997
 
                 end_revision=len(new_rh))
 
1932
                 start_revision=base_idx+1,
 
1933
                 end_revision=len(new_rh),
 
1934
                 search=None)
1998
1935
 
1999
1936
 
2000
1937
def get_history_change(old_revision_id, new_revision_id, repository):
2066
2003
    log_format = log_formatter_registry.get_default(branch)
2067
2004
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
2068
2005
    if old_history != []:
2069
 
        output.write('*' * 60)
 
2006
        output.write('*'*60)
2070
2007
        output.write('\nRemoved Revisions:\n')
2071
2008
        show_flat_log(branch.repository, old_history, old_revno, lf)
2072
 
        output.write('*' * 60)
 
2009
        output.write('*'*60)
2073
2010
        output.write('\n\n')
2074
2011
    if new_history != []:
2075
2012
        output.write('Added Revisions:\n')
2076
2013
        start_revno = new_revno - len(new_history) + 1
2077
 
        show_log(branch, lf, verbose=False, direction='forward',
 
2014
        show_log(branch, lf, None, verbose=False, direction='forward',
2078
2015
                 start_revision=start_revno)
2079
2016
 
2080
2017
 
2086
2023
    :param last_revno: The revno of the last revision_id in the history.
2087
2024
    :param lf: The log formatter to use.
2088
2025
    """
 
2026
    start_revno = last_revno - len(history) + 1
2089
2027
    revisions = repository.get_revisions(history)
2090
2028
    for i, rev in enumerate(revisions):
2091
2029
        lr = LogRevision(rev, i + last_revno, 0, None)
2092
2030
        lf.log_revision(lr)
2093
2031
 
2094
2032
 
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.
 
2033
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2034
    """Find file-ids and kinds given a list of files and a revision range.
2097
2035
 
2098
2036
    We search for files at the end of the range. If not found there,
2099
2037
    we try the start of the range.
2102
2040
    :param file_list: the list of paths given on the command line;
2103
2041
      the first of these can be a branch location or a file path,
2104
2042
      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.
 
2043
    :param add_cleanup: When the branch returned is read locked,
 
2044
      an unlock call will be queued to the cleanup.
2107
2045
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2108
 
      info_list is a list of (relative_path, found, kind) tuples where
 
2046
      info_list is a list of (relative_path, file_id, kind) tuples where
2109
2047
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2110
2048
      branch will be read-locked.
2111
2049
    """
2112
2050
    from breezy.builtins import _get_revision_range
2113
2051
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2114
2052
        file_list[0])
2115
 
    exit_stack.enter_context(b.lock_read())
 
2053
    add_cleanup(b.lock_read().unlock)
2116
2054
    # XXX: It's damn messy converting a list of paths to relative paths when
2117
2055
    # those paths might be deleted ones, they might be on a case-insensitive
2118
2056
    # filesystem and/or they might be in silly locations (like another branch).
2127
2065
        relpaths = [path] + file_list[1:]
2128
2066
    info_list = []
2129
2067
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2130
 
                                                       "log")
 
2068
        "log")
2131
2069
    if relpaths in ([], [u'']):
2132
2070
        return b, [], start_rev_info, end_rev_info
2133
2071
    if start_rev_info is None and end_rev_info is None:
2135
2073
            tree = b.basis_tree()
2136
2074
        tree1 = None
2137
2075
        for fp in relpaths:
2138
 
            kind = _get_kind_for_file(tree, fp)
2139
 
            if not kind:
 
2076
            file_id = tree.path2id(fp)
 
2077
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2078
            if file_id is None:
2140
2079
                # go back to when time began
2141
2080
                if tree1 is None:
2142
2081
                    try:
2143
2082
                        rev1 = b.get_rev_id(1)
2144
2083
                    except errors.NoSuchRevision:
2145
2084
                        # No history at all
 
2085
                        file_id = None
2146
2086
                        kind = None
2147
2087
                    else:
2148
2088
                        tree1 = b.repository.revision_tree(rev1)
2149
2089
                if tree1:
2150
 
                    kind = _get_kind_for_file(tree1, fp)
2151
 
            info_list.append((fp, kind))
 
2090
                    file_id = tree1.path2id(fp)
 
2091
                    kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2092
            info_list.append((fp, file_id, kind))
2152
2093
 
2153
2094
    elif start_rev_info == end_rev_info:
2154
2095
        # One revision given - file must exist in it
2155
2096
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2156
2097
        for fp in relpaths:
2157
 
            kind = _get_kind_for_file(tree, fp)
2158
 
            info_list.append((fp, kind))
 
2098
            file_id = tree.path2id(fp)
 
2099
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2100
            info_list.append((fp, file_id, kind))
2159
2101
 
2160
2102
    else:
2161
2103
        # Revision range given. Get the file-id from the end tree.
2167
2109
            tree = b.repository.revision_tree(rev_id)
2168
2110
        tree1 = None
2169
2111
        for fp in relpaths:
2170
 
            kind = _get_kind_for_file(tree, fp)
2171
 
            if not kind:
 
2112
            file_id = tree.path2id(fp)
 
2113
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2114
            if file_id is None:
2172
2115
                if tree1 is None:
2173
2116
                    rev_id = start_rev_info.rev_id
2174
2117
                    if rev_id is None:
2176
2119
                        tree1 = b.repository.revision_tree(rev1)
2177
2120
                    else:
2178
2121
                        tree1 = b.repository.revision_tree(rev_id)
2179
 
                kind = _get_kind_for_file(tree1, fp)
2180
 
            info_list.append((fp, kind))
 
2122
                file_id = tree1.path2id(fp)
 
2123
                kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2124
            info_list.append((fp, file_id, kind))
2181
2125
    return b, info_list, start_rev_info, end_rev_info
2182
2126
 
2183
2127
 
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
 
2128
def _get_kind_for_file_id(tree, path, file_id):
 
2129
    """Return the kind of a file-id or None if it doesn't exist."""
 
2130
    if file_id is not None:
 
2131
        return tree.kind(path, file_id)
 
2132
    else:
 
2133
        return None
2191
2134
 
2192
2135
 
2193
2136
properties_handler_registry = registry.Registry()
2194
2137
 
2195
2138
# Use the properties handlers to print out bug information if available
2196
 
 
2197
 
 
2198
2139
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
 
2140
    if 'bugs' in revision.properties:
 
2141
        bug_lines = revision.properties['bugs'].split('\n')
 
2142
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2143
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2144
                          len(row) > 1 and row[1] == 'fixed']
2215
2145
 
 
2146
        if fixed_bug_urls:
 
2147
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2148
                    ' '.join(fixed_bug_urls)}
 
2149
    return {}
2216
2150
 
2217
2151
properties_handler_registry.register('bugs_properties_handler',
2218
2152
                                     _bugs_properties_handler)